# 完整工单流转手册(信号级) > 本文档按 **信号 → 执行单元 → 状态变化** 的粒度,完整描述一个工单从建单到 > 全部工件下线的全过程:每一步"发生了什么、执行哪个最小单元、结果状态是什么 > (job 在哪/什么步骤、槽位谁占用、锁谁持有)"。 > > 对应实现:`internal/line/executor.go`(任务→PLC 动作映射)、`internal/plcactions/executor.go` > (信号序列)、`internal/device/*.go`(各设备模块状态推进)、`internal/robot/` > (任务队列/优先级仲裁/预约制)、`internal/line/priority.go`(优先级配置加载)、 > `doc/signals.sql`(信号表)。 --- ## 一、产线拓扑与资源 | 设备 | 类型码 | 槽位 | 说明 | |------|--------|------|------| | 接驳台 ×2 | DOCK | 各 42 槽 | 毛坯入口/成品出口;工件绑定取料槽位 | | 磨床 | GRINDER | 1 槽 | 配油盘,M3600 加工完成 | | 换向机构 | TURN_TABLE | 1 槽 | 方形件换向(PLC 自动旋转) | | 气吹清洗 | AIR_BLOW | 1 槽 | 持料模型:机器人夹持吹洗,吹洗位不持工件 | | 全检台 | FULL_CHECK | 6 槽 | 人工检验,批次检测(M2098 广播) | | 缓存台 | CACHE | 1 槽 | 全检满时暂存待检件 | | 机器人 | TEMP_SLOT(夹爪) | — | 全厂唯一共享资源,持料锁 | **资源锁(resource_lock)**: - `robot`:任务派发时持有、完成时释放(单在途任务互斥) - `temp_slot`(夹爪):取料完成后归取料 job;放料完成释放;换料交接转移给前任 --- ## 二、PLC 信号体系(M 区地址) 约定:**PC→PLC** 为软件触发信号(置 1 后 PLC 自复位);**PLC→PC** 为完成/状态通知 (信号轮询器每 1 秒读一遍全部状态信号、上升沿触发,speedFactor 加速时最快 50ms)。 每组操作前必须先写 **M2022 工件类型**。 > 地址均为当前 DB signal 表种子配置(`doc/signals.sql`),程序启动时加载, > 代码只写信号名、不写地址(详见 4.0)。 ### 接驳台(M2000-M2019) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2000 | DockFetchReq | PC→PLC | 取料请求(byte,触发) | | M2001 | DockFetchPos | PC→PLC | 取位置号(1-42) | | M2002 | DockFetchTray | PC→PLC | 取料台号(1/2) | | M2009 | DockFetchDone | PLC→PC | 取料完成 | | M2010 | DockPlaceReq | PC→PLC | 放料请求(byte,触发) | | M2011 | DockPlacePos | PC→PLC | 放位置号(1-42) | | M2012 | DockPlaceTray | PC→PLC | 放料台号(1/2) | | M2019 | DockPlaceDone | PLC→PC | 放料完成 | ### 相机(M2020-M2029) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2022 | CameraWorkType | PC→PLC | 工件类型(每组操作前必写) | | M2027 | CameraShootResult | PLC→PC | 拍照结果(1=有料,0=无料) | | M2029 | CameraShootDone | PLC→PC | 拍照完成 | ### 磨床(M2030-M2059) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2030 | GrinderFetchReq | PC→PLC | 取料请求 | | M2039 | GrinderFetchDone | PLC→PC | 取料完成 | | M2040 | GrinderPlaceReq | PC→PLC | 放料请求 | | M2049 | GrinderPlaceDone | PLC→PC | 放料完成 | | M2050 | GrinderExchangeReq | PC→PLC | 换料请求 | | M2059 | GrinderExchangeDone | PLC→PC | 换料完成 | ### 换向机构(M2060-M2079) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2060 | TurnTableFetchReq | PC→PLC | 取料请求 | | M2069 | TurnTableFetchDone | PLC→PC | 取料完成 | | M2070 | TurnTablePlaceReq | PC→PLC | 放料请求 | | M2079 | TurnTablePlaceDone | PLC→PC | 放料完成 | | M3601.0/1 | TurnTableClamped/Released | PLC→PC | 夹紧/松开到位 | ### 全检换料(M2080-M2089) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2081 | FullCheckExchangeFetchPos | PC→PLC | 换料取位置号 | | M2082 | FullCheckExchangePlacePos | PC→PLC | 换料放位置号 | | M2080 | FullCheckExchangeReq | PC→PLC | 换料请求 | | M2089 | FullCheckExchangeDone | PLC→PC | 换料完成 | ### 全检台(M2090-M2109) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2091 | FullCheckFetchPos | PC→PLC | 取位置号 | | M2090 | FullCheckFetchReq | PC→PLC | 取料请求 | | M2099 | FullCheckFetchDone | PLC→PC | 取料完成 | | M2101 | FullCheckPlacePos | PC→PLC | 放位置号 | | M2100 | FullCheckPlaceReq | PC→PLC | 放料请求 | | M2109 | FullCheckPlaceDone | PLC→PC | 放料完成 | | M2102 | FullCheckPlaceDoneRequestInspect | PC→PLC | 放料完成请求检验(纯脉冲不等) | | M2098 | FullCheckInspectDone | PLC→PC | 检测完成(批次广播,全槽转 DONE) | | M2108 | FullCheckDetecting | PLC→PC | 人工检验中 | ### 缓存台(M2110-M2129) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2111 | CacheFetchPos | PC→PLC | 取料位置号 | | M2110 | CacheFetchReq | PC→PLC | 取料请求 | | M2119 | CacheFetchDone | PLC→PC | 取料完成 | | M2121 | CachePlacePos | PC→PLC | 放料位置号 | | M2120 | CachePlaceReq | PC→PLC | 放料请求 | | M2129 | CachePlaceDone | PLC→PC | 放料完成 | ### 清洗(M2130-M2139) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M2130 | WashStartReq | PC→PLC | 清洗请求(写 1 触发) | | M2139 | AirBlowCompleteDone | PLC→PC | 清洗完成(**byte 型**) | ### 设备状态位(M3600.x) | 地址 | 名称 | 方向 | 含义 | |------|------|------|------| | M3600.0 | GrinderMachiningDone | PLC→PC | 磨床加工完成 | | M3600.1 | GrinderAllowEntry | PLC→PC | 磨床允许进入 | | M3604.0/1 | Dock1Present/Dock2Present | PLC→PC | 接驳台在位 | --- ## 三、状态机总览 ### job 状态(status) `CREATED(建单)` → `IN_HANDLING/PROCESSING/WAITING_UNLOAD/WAITING_DECISION`(流转中) → `COMPLETED(收尾)`;`SCRAPPED(报废)/SUSPENDED(暂停)` 为终态/挂起。 ### job 步骤(current_step_id,圆形件主线) ``` OP10_LOAD(接驳台取料) → OP15_DECIDE(持毛坯等磨床) → OP20_LOAD/EXCHANGE(上磨床) → OP20_WAIT(等M3600) → OP30_LOAD(成品入夹爪待清洗) → OP35_DECIDE(清洗完成持料) → OP40_LOAD/EXCHANGE(放全检/换料) → OP40_BATCH_DECIDE(批次决策) → OP40_REQ(已发M2102) → OP40_WAIT(待检) → OP40_UNLOAD(检测完成待取) → OP60_LOAD(下料接驳台) → COMPLETED ``` 方形件在 OP10 后先走 `OP25_TURN_LOAD → OP25_TURN_UNLOAD`(换向)再进磨床。 ### 槽位状态(equipment_slot.status) `EMPTY` →(预约)→ `PROCESSING` →(落料完成)→ `OCCUPIED` →(M3600/M2098)→ `DONE` →(取走)→ `EMPTY`。换料任务预约 **DONE** 槽位(取成品放毛坯一步完成)。 ### 夹爪锁(temp_slot) `free` →(取料完成 acquire)→ `held_by_job` →(放料完成 release / 换料交接 handover)→ ... --- ## 四、最小执行单元全集(信号级) ### 4.0 通用骨架:一次 PLC 握手到底发生了什么 机器人单爪串行作业:一个最小执行单元 = 一个搬运任务 = "从 A 点取件(装载)→ 机器人物理搬运(代码不参与)→ 放到 B 点(卸载)", 或**换料(交换式)**:到 A 点把设备里的**加工完成品**取走、同时把夹爪里 持着的工件放进 A 点——一个 Req 同时完成取+放(U11 磨床 / U12 全检 / 二次换料)。**注意夹爪里的工件不一定是毛坯**:磨床换料时是毛坯,全检 换料与二次换料时是成品(待检成品/缓存暂存成品),链式连续换料见 4.2。 **代码只负责两端与 PLC 的信号握手**,中间的物理搬运由机器人自行完成, 软件以 done 信号到达作为任务完成标志。 一次完整握手的六步(代码 `signalActionWithParams`,约束见 agent约束 6.4.1), 以 U1 接驳台取料为例逐步拆解: | 步 | 代码动作 | U1 实例 | 为什么这么做 | |----|---------|---------|-------------| | 1 | 写工件类型 | 写 `CameraWorkType` = 产品配方 ID | PLC 按工件类型切换抓手/程序参数,每组动作前必须告知 | | 2 | 连续写参数 | 写 `DockFetchPos`=槽号、`DockFetchTray`=台号 | 两个值是给同一个取料动作用的数据,**参数之间不停顿、连续写** | | 3 | 空 1 秒 | `sleep(1s)` | 给 PLC 一个扫描周期消化参数(S7 通信异步),否则 Req 到了参数还没生效 | | 4 | 写 Req=1 | 写 `DockFetchReq`=1 | 触发 PLC 执行取料。**只写 1 不写 0**:动作完成后 PLC 自行复位 | | 5 | 空 1 秒 | `sleep(1s)` | 防止紧接着的下一次写操作覆盖本次写入 | | 6 | 等 done | 等 `DockFetchDone` 上升沿 | 见下方"done 怎么等" | **done 怎么等(不是轮询)**:派车时先把 done 信号名登记进 `doneWaits` 表 (在发 Req **之前**登记——因为第 5 步的 1 秒间隔里 PLC 往往已干完并回了 done,后登记会错过上升沿导致任务永久等待)。此后完全事件驱动: PLC done 信号上升沿 → 信号轮询器 → `onSignal` → 匹配 `doneWaits` → `OnTaskDone` 广播任务完成。**没有 300ms 轮询**。 **M 地址从哪来?配置粒度分两层,如实说明**: 1. **地址层(M2000/M2009…)已配置化**:地址存在 **DB signal 表**(种子数据 `doc/signals.sql`,id=1 行即 DockFetchReq → M2000)。程序启动时 preload 全量加载进内存,执行时按信号名查地址,查不到直接报错"信号地址未配置, 请检查DB signal表"。**改地址只改 DB,不用改代码**。 2. **名称层(`DockFetchReq`…)仍是代码硬编码**:所有信号名集中定义在 `constants` 包 `Sig*` 常量(一处定义,执行器全部引用常量,不再散落 魔法字符串)。**改信号名仍需同步改 constants + DB**——"只改 DB"只对 地址层成立。 3. **动作→信号组映射**(如 DockFetch 动作 = Req+Done+Pos+Tray 一组)也 硬编码在 `plcactions` 方法内;如需现场按动作配置信号组,需给 signal 表 增加动作维度(架构级改造,未做)。 下文每个单元的序列按 `信号名(当前配置地址)` 书写:括号内地址是当前种子 配置值,现场可调(改 DB);信号名对应 constants 常量(改代码)。 ### 4.1 前置条件:生成 vs 预约(两层,缺一不可) 每个单元的"前置"分两层,全部满足任务才会真正派车执行: **第一层 · 生成条件**:设备模块周期巡检(`Evaluate`)时,按槽位状态 + job 步骤判断"现在该干什么",生成任务**入队**。此时槽位状态没有任何变化。 **第二层 · 预约通过**:机器人仲裁器从队头取任务,派车前调用 `line.reserve`: - 目的=夹爪(一切取料类任务):查 `temp_slot` 锁。锁被**其他 job** 持有 → 预约失败,任务留队等下一轮(单爪防覆盖);free 或本人持 → 通过。 - 目的=设备槽位(放料/换料/清洗类):开事务 `SELECT ... FOR UPDATE` 锁住 槽位行(防并发抢占)→ 校验槽位状态符合预期(**换料预约 DONE**,其余 预约 EMPTY)→ 通过则写 **EMPTY/DONE → PROCESSING** + `current_job_id` =本 job → 提交。状态不符 → 预约失败留队。 **槽位归属就是这一步写下的**,任务 done 时再由模块推进到 OCCUPIED。 - job 已终态(COMPLETED/SCRAPPED/CANCELLED)→ 任务直接作废丢弃。 **重点**:取料类任务**不会**把源槽位预约为 PROCESSING——源槽位只是任务 生成的依据(OCCUPIED/DONE),任务 done 时由模块直接写回 EMPTY。 下文每个单元按 `生成:…;预约:…` 两行书写前置。 ### 4.2 换料与连续换料链(U11 / U12 / U15 的关系) **一次换料 = 取成品 + 放夹爪工件同步完成**:一个 Req(磨床 M2050 / 全检 M2080)让 PLC 同时完成"取出设备里的加工完成品 + 把夹爪工件放进 空出的位置",夹爪工件随即变成设备里取出的成品(temp_slot 交接给成品 job)。三种换料的夹爪持件不同: | 换料 | 夹爪持 | 设备槽 | 完成效果 | |------|--------|--------|---------| | U11 磨床换料 | **毛坯**(本 job) | 磨床 DONE 成品 | 毛坯开磨;成品入夹爪(交接给前任) | | U12 全检换料 | **待检成品**(本 job,清洗完) | 全检 DONE 成品 | 待检件落全检;done 成品入夹爪(交接给前任) | | 二次换料(U12 同序列) | **缓存暂存成品**(代做链 job) | 全检 DONE 成品 | 缓存件落全检;done 成品入夹爪(交接给 ProxyJobID 成品 job) | **连续换料链(continuous_exchange)**:换料任务把 done 槽的成品带进夹爪 后,链 job 不是直接放全检,而是**代下料 → 回链决策点 → 再换**——每轮把 一件成品从全检 done 槽(或缓存)"换"出并下料,磨床不停机、全检不空闲: ``` 链首 job(B):U12 换料(B 待检件→全检槽,A 成品入夹爪) → U9 代下料 A → 链决策点 OP61_DECIDE → 缓存有件:U15 代取缓存成品 C → 二次换料(C 落全检槽,D 成品入夹爪) → U9 代下料 D → 回 OP61_DECIDE,循环 → 缓存无件 或 全检已满 2 件:链结束 → 批次决策(M2102) ``` 全检模块每轮评估的决策优先级(evaluateTx,从上到下命中即止): 1. done 槽 + 持待检件 → **U12 换料**(最高优先,成品 carry 下料) 2. done 槽 + 二次换料等待 job(proxy 标记)→ 补发二次换料 3. 代做链 OP61_DECIDE:全检已满 2 件 → 批次决策;done 槽 + 缓存有件 → U15 代取;缓存无件 → 链结束回批次决策;无 done → 等待(幂等重评), **但本工单最后一件除外:回批次决策触发 M2102 收尾**(2026-08-17 修复: 最后一件无后续换料/取料事件可等,原逻辑待检件永久等待) 4. done 槽 + **无换料链活跃** → U4 自取(链活跃时抑制自取,防止 done 槽 被抢走导致二次换料链断裂;链 job 离开换料步骤后恢复自取) 5. 空位 → U7 放料;全满 → U10 缓存暂存 ### 4.3 任务队列与优先级仲裁(谁先派车) **队列模型**:全部设备模块(接驳台/磨床/转台/全检/缓存)生成的任务进入**同一个** 机器人任务队列(`robot.Queue`),仲裁器(`robot.Arbiter`)从队头取任务 → 预约目的地成功 → 派车。机器人单爪,**同一时刻至多一个在途任务**;预约失败的 任务留队下一轮、让位队尾(`EnqueueBack`),保证每轮扫描覆盖全队不饿死新任务。 **优先级规则**(`robot_task_priority` 表,数据驱动;数值越小越优先,改表重启即生效): **组合优先级(2026-08-17 定稿)**:优先级语义为「任务类型 + 搬运源设备 + 目标设备」 三要素组合——一条搬运路径(如换向台→磨床链)是否优先由组合决定,任何设备组合 可单独配置,无需发明新任务类型。配置行 `from_type`/`to_type` 为 NULL 表示通配; 查找顺序(最具体优先):精确 (kind,from,to) → (kind,from) → (kind,to) → kind-only → 内置默认。 | 组合键(kind, from, to) | 默认优先级 | 典型任务 | 业务含义 | |---------|-----------|---------|---------| | EXCHANGE | 0 | U11 磨床换料 / U12 全检换料 | 磨床不停机、全检链不断,最高 | | **(UNLOAD, TURN_TABLE)** | **5** | **U3 换向取料** | **换向机构与磨床同链:换向件优先取走供磨床,磨床空时优先供料(组合优先)** | | LOAD | 10 | U6 磨床放料 / U7 全检放料 / U8 转台放料 / U9 接驳台放料 | 上料类 | | UNLOAD | 10 | U1 接驳台取料 / U2 磨床取料 / U4 全检取料 / U5 缓存取料 | 卸料类 | | WASH | 10 | U13 清洗 | 持料模型 | | RELOAD | 20 | U14 缓存回流 | 回流 | | TRANSFER | 30 | U15 代取缓存件 | 普通转运 | **同优先级 FIFO**:相同优先级的任务按入队时间先进先出(先入队先派车)。 **计数老化防饿死(确定性,非墙钟时间)**:任务每预约失败让位一次(`YieldCount` +1), 有效优先级提升一级(下限 0 与最高优先级平齐)——让位次数是**事件序**而非时间: 同样的状态序列必然产生同样的让位次数,调度结果可复现(正向状态机语义), 同时低优先级任务最终必然被调度,不会无限排队。 **持料隐式优先级**:夹爪被某 job 持有时,其他取料类任务(目的=夹爪)预约失败 → 让位队尾;持料 job 的下一步任务(放料/换料/清洗)反而畅通——持料模型天然 把机器人让给"已持有工件"的 job 优先走完,这是优先级之外的隐式排序。 **换向取料组合优先级(业务硬规则,2026-08-17 修复)**: - 换向台有换向件(转台槽 DONE)时,**必须先取走换向件供磨床** (组合行 `('UNLOAD','TURN_TABLE',NULL)`=5),不能先派接驳台取毛坯 (UNLOAD 通配=10)——换向机构与磨床同属磨床链,磨床空转等待会拖慢整线节拍。 - 历史缺陷:换向取料原与接驳台取料同为 UNLOAD(10),FIFO 下换向件排队滞留 转台、磨床空等(实测:TURN-FETCH 入队排第 6 位,DOCK-FETCH 先执行)。 - 修复演进:先新增 TURN_FETCH 任务类型(优先级 5)应急 → 被否(组合语义压扁成 单模块语义,无法覆盖"换向+其他设备"组合)→ 定稿为组合优先级:UNLOAD 类型 不变,换向取料优先由组合行表达,任意设备组合可配,不再发明新类型。 --- ### U1 接驳台取料(DOCK→夹爪,毛坯上线) - **生成**:job 停在首道工序 OP10_LOAD(StepDockFetch)且已绑定接驳台槽位 (AGV/人工放毛坯后槽位 OCCUPIED);未绑槽则静默跳过等送料 - **预约**:目的=夹爪 → `temp_slot` 必须 free(或本 job 已持); **源槽位不预约**(仍是 OCCUPIED,不是 PROCESSING) - **序列**:`CameraWorkType`(M2022)=配方ID → `DockFetchPos`(M2001)=槽号 → `DockFetchTray`(M2002)=台号 → 1s → `DockFetchReq`(M2000)=1 → 1s → 等 `DockFetchDone`(M2009) - **结果**:接驳台槽→EMPTY;夹爪→持本 job 毛坯(temp_slot 上锁);job→ OP15_DECIDE(圆形件,持毛坯等磨床)/ OP25_TURN_LOAD(方形件,先去换向) ### U2 磨床取料(GRINDER→夹爪,断料收尾专用) - **生成**(磨床模块三条同时成立): 1. 磨床槽 **DONE**——M3600.0 上升沿(磨削完成)时模块把槽 OCCUPIED→DONE, 成品仍挂在原 job 名下(current_job_id),job 停在 OP20_WAIT; 2. **没有待磨件**(无 job 停在 OP15_DECIDE 持毛坯等磨床); 3. **没有在途磨床任务**(防重复评估重复生成)。 含义:磨完这一件后没有下一件毛坯可换,成品自己取走去清洗(断料收尾)。 - **预约**:目的=夹爪 → `temp_slot` 必须 free - **序列**:`CameraWorkType`(M2022) → `GrinderFetchReq`(M2030)=1 → 1s → 等 `GrinderFetchDone`(M2039) - **结果**:磨床槽→EMPTY;夹爪→持本 job 成品;job→OP30_LOAD(持成品待清洗) - **对比**:有毛坯待磨时磨床 DONE **不走本单元**,走 U11 换料(毛坯进、 成品出、磨床不停机) ### U3 转台取料(TURN_TABLE→夹爪,方形件换向后取回) - **生成**:转台槽 **DONE** 且 job 在 OP25_TURN_UNLOAD——放料任务 done 时 槽位直接 OCCUPIED→DONE(TurnTablePlaceDone 到达即放料+换向完成), 模块随即生成取料任务 - **任务类型**:UNLOAD(组合优先级 `(UNLOAD, TURN_TABLE)`=5,配置表驱动)—— 换向机构与磨床同链,换向件优先取走供磨床,高于普通卸料(UNLOAD 通配=10), 规则详见 4.3 - **预约**:目的=夹爪 → `temp_slot` 必须 free - **序列**:`CameraWorkType`(M2022) → `TurnTableFetchReq`(M2060)=1 → 1s → 等 `TurnTableFetchDone`(M2069) - **结果**:转台槽→EMPTY;夹爪→持本 job 毛坯;job→OP15_DECIDE(继续去磨床) ### U4 全检取料(FULL_CHECK→夹爪,成品出全检) - **生成**:全检槽 DONE(M2098 批次检测完成时 OCCUPIED→DONE)且 job 在 OP40_UNLOAD;且**无换料链活跃**(有链 job 时成品由换料带走,不单独取料) - **预约**:目的=夹爪 → `temp_slot` 必须 free - **序列**:`CameraWorkType`(M2022) → `FullCheckFetchPos`(M2091)=槽号 → 1s → `FullCheckFetchReq`(M2090)=1 → 1s → 等 `FullCheckFetchDone`(M2099) - **结果**:全检槽→EMPTY;夹爪→持本 job 成品;job→OP60_LOAD(待下料) ### U5 缓存取料(CACHE→夹爪,回流第一步) - **生成**:缓存槽 OCCUPIED(全检满时暂存的待检件)且 job 在 OP50_UNLOAD (待回流) - **预约**:目的=夹爪 → `temp_slot` 必须 free - **序列**:`CameraWorkType`(M2022) → `CacheFetchPos`(M2111)=槽号 → 1s → `CacheFetchReq`(M2110)=1 → 1s → 等 `CacheFetchDone`(M2119) - **结果**:缓存槽→EMPTY;夹爪→持本 job 待检件;job→OP40_LOAD(待放全检) ### U6 磨床放料(夹爪→GRINDER,毛坯上磨床) - **生成**:磨床槽 EMPTY 且 job 持毛坯在 OP15_DECIDE - **预约**:目的=磨床槽 → FOR UPDATE 锁行 → **EMPTY→PROCESSING(owner=job)** - **序列**:`CameraWorkType`(M2022) → `GrinderPlaceReq`(M2040)=1 → 1s → 等 `GrinderPlaceDone`(M2049) - **结果**:磨床槽→OCCUPIED(owner=job,磨削开始);夹爪释放;job→OP20_WAIT (等 M3600) ### U7 全检放料(夹爪→FULL_CHECK,成品送检) - **生成**:全检有空槽 EMPTY 且 job 持成品在 OP35_DECIDE(清洗完持料等容量) - **预约**:目的=全检空槽 → **EMPTY→PROCESSING(owner=job)** - **序列**:`CameraWorkType`(M2022) → `FullCheckPlacePos`(M2101)=槽号 → 1s → `FullCheckPlaceReq`(M2100)=1 → 1s → 等 `FullCheckPlaceDone`(M2109) - **结果**:全检槽→OCCUPIED(owner=job);夹爪释放;job→OP40_BATCH_DECIDE: 批次已满 2 件或工单最后一件 → 立即发 `FullCheckPlaceDoneRequestInspect` (M2102)=1 脉冲请求检验 → job→OP40_REQ;否则→OP40_WAIT 等下一件凑批 ### U8 转台放料(夹爪→TURN_TABLE,方形件换向) - **生成**:转台槽 EMPTY 且 job 持毛坯在 OP25_TURN_LOAD - **预约**:目的=转台槽 → **EMPTY→PROCESSING(owner=job)** - **序列**:`CameraWorkType`(M2022) → `TurnTablePlaceReq`(M2070)=1 → 1s → 等 `TurnTablePlaceDone`(M2079) - **结果**:转台槽→**DONE**(放料协议含 PLC 自动旋转,done 即换向完成); 夹爪释放;job→OP25_TURN_UNLOAD(随后立即生成 U3 取回) ### U9 接驳台放料(夹爪→DOCK,成品下线) - **生成**:接驳台有 EMPTY 槽且 job 持成品在 OP60_LOAD - **预约**:目的=接驳台槽 → **EMPTY→PROCESSING**;代下料场景槽位归属 PrevJobID(成品是谁的就归谁),自行下料归本人 - **序列**:`CameraWorkType`(M2022) → `DockPlacePos`(M2011)=槽号 → `DockPlaceTray`(M2012)=台号 → 1s → `DockPlaceReq`(M2010)=1 → 1s → 等 `DockPlaceDone`(M2019) - **结果**:接驳台槽→OCCUPIED;夹爪释放;job→**COMPLETED** (FinishJob:finished_num+1,全部完成则工单 COMPLETED) ### U10 缓存放料(夹爪→CACHE,全检满暂存) - **生成**:全检 6 槽全满且缓存有空槽;job 持成品在 OP35_DECIDE - **预约**:目的=缓存槽 → **EMPTY→PROCESSING(owner=job)** - **序列**:`CameraWorkType`(M2022) → `CachePlacePos`(M2121)=槽号 → 1s → `CachePlaceReq`(M2120)=1 → 1s → 等 `CachePlaceDone`(M2129) - **结果**:缓存槽→OCCUPIED;`job.temp_slot_no` 记录暂存槽;夹爪释放; job→OP50_UNLOAD(待回流,全检出空后走 U5+U7 回流) ### U11 磨床换料(夹爪毛坯 ↔ GRINDER 成品,磨床不停机) - **生成**:磨床槽 DONE 且**有**待磨件(job 持毛坯在 OP15_DECIDE) ——与 U2 互斥:有待磨件走换料,无待磨件走取料 - **预约**:目的=磨床槽 → FOR UPDATE 锁行 → **DONE→PROCESSING(owner=本 job)**;派车时校正 PrevJobID=槽位原主人(成品 job) - **序列**:`CameraWorkType`(M2022) → `GrinderExchangeReq`(M2050)=1 → 1s → 等 `GrinderExchangeDone`(M2059) - **结果**:磨床槽→OCCUPIED(owner=本 job,新毛坯开磨);**夹爪交接**:本 job 毛坯已放进磨床,夹爪转为持 PrevJobID 成品(temp_slot 锁转移);本 job→ OP20_WAIT;PrevJobID→OP30_LOAD(持成品待清洗) ### U12 全检换料(夹爪待检件 ↔ FULL_CHECK 成品) - **生成**:全检有 done 槽,且存在换料链 job——持成品在 OP35_DECIDE 的 job 被判定为链首,done 槽归属链前任(成品 job) - **预约**:目的=全检 done 槽 → **DONE→PROCESSING(owner=本 job)** - **序列**:`CameraWorkType`(M2022) → `FullCheckExchangeFetchPos`(M2081)=槽号 → 1s → `FullCheckExchangePlacePos`(M2082)=槽号 → 1s → `FullCheckExchangeReq`(M2080)=1 → 1s → 等 `FullCheckExchangeDone`(M2089) (注:三条写信号之间**必须**各空 1 秒,与配方 wait_after_ms 一致, 缺间隔生产 PLC 会信号未生效即写下一条) - **结果**:done 槽→OCCUPIED(owner=本 job);成品入夹爪(PrevJobID 持有)→ OP60_EXCHANGE_LOAD(链 job 代下料);本 job→OP40_BATCH_DECIDE(待检件 已落全检,走批次决策) ### U13 清洗(夹爪持成品 → AIR_BLOW,持料模型) - **生成**:吹洗位可用且 job 持成品在 OP30_LOAD - **预约**:目的=吹洗位 → **EMPTY→PROCESSING**(清洗位物理不持工件,洗完 仍夹在夹爪上) - **序列**:`CameraWorkType`(M2022) → `WashStartReq`(M2130)=1 → 1s → 等 `AirBlowCompleteDone`(M2139)(**信号名是 AirBlowCompleteDone**, 旧名 WashCompleteDone 已不在信号表中,byte 型) - **结果**:吹洗位→EMPTY;**夹爪不释放(持料模型)**;job→OP35_DECIDE (持成品等全检容量决策) ### U14 缓存回流(CACHE → FULL_CHECK,两步串行) - **生成**:缓存槽 OCCUPIED(owner=job) 且全检有空槽;job 在 OP50_UNLOAD - **预约**:目的=全检空槽 → **EMPTY→PROCESSING(owner=job)**(取缓存这半程 的夹爪锁由任务本身保证) - **序列**:U5 序列(`CacheFetchPos`(M2111)→`CacheFetchReq`(M2110)→等 `CacheFetchDone`(M2119))→ 立刻 U7 序列(`FullCheckPlacePos`(M2101)→ `FullCheckPlaceReq`(M2100)→等 `FullCheckPlaceDone`(M2109))→ `FullCheckPlaceDoneRequestInspect`(M2102) 脉冲(完成信号=放料完成) - **结果**:缓存槽→EMPTY;全检槽→OCCUPIED(owner=job);夹爪释放;job→ OP40_BATCH_DECIDE ### U15 代取缓存件(CACHE→夹爪,换料链代做) - **生成**:全检二次换料链需要缓存里的成品(缓存槽 OCCUPIED(owner= ProxyJobID)),由链 job 持夹爪代取 - **预约**:目的=夹爪 → `temp_slot` 已由链 job 持有(本人持→通过) - **序列**:`CameraWorkType`(M2022) → `CacheFetchPos`(M2111)=槽号 → 1s → `CacheFetchReq`(M2110)=1 → 1s → 等 `CacheFetchDone`(M2119) - **结果**:缓存槽→EMPTY;夹爪→链 job 持缓存成品;job→OP40_EXCHANGE_2ND (持缓存件去做二次换料) --- ## 五、完整工单流转(示例:11 件圆形件,1 磨床 / 6 槽全检 / 1 槽缓存 / 1 机器人) ### 阶段 0:建单与放料 | 步骤 | 发生 | 结果状态 | |------|------|---------| | 0.1 | 新建工单,11 件圆形件(product_type=1) | 11 个 job=CREATED;工单=CREATED | | 0.2 | 接驳台放毛坯:AGV/人工把 11 件放接驳台各槽 | 接驳台槽1-11=OCCUPIED(各绑定 job1-11);磨床=EMPTY;全检 6 槽=EMPTY;缓存=EMPTY;temp_slot=free;robot=idle | ### 阶段 1:首件上线(接驳台→磨床) | 步骤 | 发生 | 结果状态 | |------|------|---------| | 1.1 | 启动工单,接驳台模块评估:job1 绑槽1 → 生成 DOCK-FETCH 任务(U1) | 任务入队(源槽位不预约,派车前才查夹爪锁) | | 1.2 | 派发 U1:M2022 → M2001=1 → M2002=1 → 1s → M2000 → 等 M2009 | robot=held | | 1.3 | **M2009 到达**:取料完成 | 接驳台槽1→EMPTY;temp_slot→job1(毛坯入夹爪);job1→OP10_LOAD→OP15_DECIDE;robot=idle | | 1.4 | 磨床模块评估:磨床 EMPTY + job1 持毛坯 → 生成 PLACE 任务(U6) | 派车前预约磨床槽 EMPTY→PROCESSING(owner=job1) | | 1.5 | 派发 U6:M2022 → M2040 → 等 M2049 | robot=held | | 1.6 | **M2049 到达**:放料完成 | 磨床槽→OCCUPIED(owner=job1);temp_slot 释放;job1→OP20_WAIT;robot=idle | | 1.7 | 磨床开始磨削 job1 | job1=PROCESSING;磨床槽=OCCUPIED(job1) | **结果**:job1 在磨床加工中;夹爪空;接驳台槽1 空(余 10 件)。 ### 阶段 2:首件完成 M3600 与换料(磨床优先) | 步骤 | 发生 | 结果状态 | |------|------|---------| | 2.1 | **PLC M3600.0 置位**(磨削完成)→ 上升沿 → 磨床模块处理 | 磨床槽 OCCUPIED→DONE(owner=job1);job1 仍 OP20_WAIT(成品在磨床等出路) | | 2.2 | 评估出路:job2 持毛坯在夹爪(OP15_DECIDE)→ 生成 EXCHANGE 任务(job2 换 job1) | 派车前预约磨床 DONE 槽→PROCESSING(owner=job2),PrevJobID 取槽位原主人 job1 | | 2.3 | 派发 U11:M2022 → M2050 → 等 M2059 | robot=held | | 2.4 | **M2059 到达**:换料完成 | 磨床槽→OCCUPIED(owner=job2);job2→OP20_WAIT(开始磨);**temp_slot 交接 job2→job1**(成品入夹爪);job1→OP30_LOAD;robot=idle | **结果**:磨床磨 job2;夹爪持 job1 成品;磨床不停机(换料即取即放)。 ### 阶段 3:清洗(持料模型)与放全检 | 步骤 | 发生 | 结果状态 | |------|------|---------| | 3.1 | 清洗模块评估:job1 持成品 → 生成 WASH 任务(U13) | 预约吹洗位 | | 3.2 | 派发 U13:M2022 → M2130 → 等 M2139 | robot=held;job1 持料贯穿(temp_slot 不释放) | | 3.3 | **M2139 到达**:清洗完成 | 吹洗位→EMPTY;**temp_slot 仍归 job1(持料模型)**;job1→OP35_DECIDE;robot=idle | | 3.4 | 全检模块评估:全检有空槽 + job1 持成品 → 生成 PLACE 任务(U7) | 派车前预约全检空槽→PROCESSING(owner=job1) | | 3.5 | 派发 U7:M2022 → M2101=槽号 → 1s → M2100 → 等 M2109 → **M2102 请求检验**(脉冲) | robot=held | | 3.6 | **M2109 到达**:放料完成 | 全检槽→OCCUPIED(owner=job1);**temp_slot 释放**;job1→OP40_BATCH_DECIDE;robot=idle | | 3.7 | 批次决策:槽1 已 OCCUPIED 但未满 2 件且非末件 → 不发 M2102,job1→OP40_WAIT | job1 在全检台待检;夹爪空 | **结果**:job1 待检;夹爪空(持料在放全检后释放);磨床磨 job2。 ### 阶段 4:批量流转(第 2-10 件循环) | 步骤 | 发生 | 结果状态 | |------|------|---------| | 4.1 | job2 M3600 → 换料给 job3(重复阶段 2);job2 清洗 → 放全检槽2(重复阶段 3) | 全检槽1=job1、槽2=job2 均 OCCUPIED;job3 磨床加工 | | 4.2 | 全检槽1+槽2 均待检 → **发 M2102 请求检验**(满 2 件批次规则) | job1/job2→OP40_REQ;人工检验开始 | | 4.3 | **PLC M2098 置位**(批次检测完成广播)→ 上升沿 | 全检槽1/2 OCCUPIED→**DONE**;job1/job2→OP40_UNLOAD(待取料) | | 4.4 | 评估:job3 持成品在 OP35_DECIDE(清洗完)→ 生成**全检换料**任务(U12:job3 换 job1) | 任务入队;派车前预约槽1(DONE)→PROCESSING(owner=job3),PrevJobID=job1 | | 4.5 | 派发 U12:M2081=槽1 → 1s → M2082=槽1 → 1s → M2080 → 等 M2089 | robot=held | | 4.6 | **M2089 到达**:全检换料完成 | 槽1→OCCUPIED(owner=job3)(待检件落全检);**成品 job1 入夹爪**(temp_slot→job1);job1→OP60_EXCHANGE_LOAD;job3→OP40_BATCH_DECIDE | | 4.7 | 全检评估:槽2 DONE 无换料者 → 生成**全检取料**任务(U4:job2 自取) | 任务入队;派车前查夹爪锁(源槽不预约) | | 4.8 | 派发 U4:M2022 → M2091=槽2 → 1s → M2090 → 等 M2099 | robot=held | | 4.9 | **M2099 到达**:取料完成 | 槽2→EMPTY;temp_slot→job2;job2→OP60_LOAD(持成品待下料) | | 4.10 | 评估:job2 持成品 → 下料任务(U9,自行下料回自己取料槽位) | 派车前预约接驳台槽2(EMPTY→PROCESSING,owner=job2) | | 4.11 | 派发 U9:M2022 → M2011=槽2 → M2012=台号 → 1s → M2010 → 等 M2019 | robot=held | | 4.12 | **M2019 到达**:下料完成 | 接驳台槽2→OCCUPIED(owner=job2);temp_slot 释放;**job2→COMPLETED**(工单 finished_num=1) | | 4.13 | job1 换料下料(job1 持成品,U9)→ COMPLETED | 接驳台槽1→OCCUPIED(owner=job1);finished_num=2 | | 4.14 | 循环:job4-10 重复阶段 2-4(磨床换料 → 清洗 → 全检换料/放料 → 批次 → 下料) | 每 2 件一批 M2102/M2098;finished_num 递增 | ### 阶段 5:缓存暂存与回流(全检 6 槽满时) | 步骤 | 发生 | 结果状态 | |------|------|---------| | 5.1 | 全检 6 槽全 OCCUPIED(待检)且 job7 清洗完成持料 | job7=OP35_DECIDE(持成品)等全检空位 | | 5.2 | 全检模块评估:无空槽 → 生成**缓存暂存**任务(U10) | 派车前预约缓存槽(EMPTY→PROCESSING) | | 5.3 | 派发 U10:M2121 → 1s → M2120 → 等 M2129 | job7 持成品去缓存 | | 5.4 | **M2129 到达**:暂存完成 | 缓存槽→OCCUPIED(owner=job7);job7.temp_slot_no=缓存槽;temp_slot 释放;job7→OP50_UNLOAD(待回流) | | 5.5 | 全检批次 M2098 → 槽1-6 DONE → 逐步取走/换料 → 出现空槽 | 全检有空槽 | | 5.6 | 缓存回流评估:缓存槽 OCCUPIED + 全检空槽 → 生成**回流**任务(U14) | 派车前预约全检空槽(EMPTY→PROCESSING) | | 5.7 | 派发 U14:U5(M2111→M2110→等 M2119)→ U7(M2101→M2100→等 M2109 + M2102) | robot=held | | 5.8 | **M2129 之后 M2109 到达**:回流完成 | 缓存槽→EMPTY;全检槽→OCCUPIED(owner=job7);temp_slot 释放;job7→OP40_BATCH_DECIDE(继续正常批次) | ### 阶段 6:全检+缓存全满的背压(no_space 场景:全检 2 槽+缓存 1 槽,15 件) | 步骤 | 发生 | 结果状态 | |------|------|---------| | 6.1 | job1/job2 磨完→清洗→放全检槽1/槽2 | 全检满(2 槽 OCCUPIED)→ 满 2 件即发 M2102 → 检验中 | | 6.2 | job3 磨完→清洗→全检满→**缓存暂存**(U10) | 缓存槽→OCCUPIED(owner=job3);job3→OP50_UNLOAD | | 6.3 | job4 磨完→清洗→全检满+缓存满→**无出路** | job4=OP35_DECIDE 持成品;temp_slot=job4(夹爪被占) | | 6.4 | job5 毛坯取料任务(U1)**预约夹爪失败**(被 job4 持)→ 留队老化 | 接驳台不再上料;没有新 job 进入 OP15_DECIDE | | 6.5 | 磨床磨完 job5 → M3600 → DONE,但**无待磨件**(无 OP15_DECIDE)→ 断料收尾生成 U2 取料任务 | U2 预约夹爪仍失败(被 job4 持)→ 留队;磨床停在 DONE 等出路 | | 6.6 | **全线背压**:磨床 DONE、夹爪被 job4 持、全检/缓存全满;唯一出路是 PLC **M2098**(批次检测完成) | 全检槽1/2→DONE;job1/job2→OP40_UNLOAD | | 6.7 | job4 持成品 → **全检换料链**(U12:job4 换 job1)→ job1 成品入夹爪→代下料(U9)→ job1 COMPLETED | 槽1→OCCUPIED(owner=job4);夹爪释放后按优先级恢复:上料/断料取料/缓存回流依次解锁 | 背压断言:暂停期间不崩溃、不空转、无死循环;恢复后槽位归属守恒。 ### 阶段 7:最后一件收尾(last_piece) | 步骤 | 发生 | 结果状态 | |------|------|---------| | 7.1 | job11 M3600(磨削完成)→ 评估:**无后继待磨件** → 不换料,生成断料取料任务(U2) | 任务入队;派车前查夹爪锁;无 EXCHANGE 任务 | | 7.2 | 取料完成:磨床槽→EMPTY;temp_slot→job11;job11→OP30_LOAD | 磨床空(收尾完成) | | 7.3 | 清洗 → 放全检(U13→U7)→ 批次决策:**工单最后一件(IsLastPieceOfOrder)→ 必发 M2102** | job11→OP40_REQ;即使不满 2 件也收尾批次 | | 7.4 | **M2098** → job11→OP40_UNLOAD → 取料(U4)→ 下料(U9) | 接驳台槽11→OCCUPIED(owner=job11);**job11→COMPLETED**;finished_num=11 | | 7.5 | FinishJob 检查:工单下全部 job 终态 → **工单→COMPLETED** | 工单完成;全部 11 件在接驳台槽位;temp_slot=free;robot=idle;磨床/全检/缓存全 EMPTY | --- ## 六、done 信号先后交替场景(done_cross) 同一时刻可能有多条 done 信号在不同设备上先后/交叉到达,验证系统不会错配、错序、漏消费: | 子场景 | 注入方式 | 系统行为 | 预期结果 | |--------|---------|---------|---------| | a. 磨床 done 迟到 | M3600.0 置位延迟 **10s**(mock 注入延迟) | 期间其他 job 正常流转(清洗/放全检不受影响);磨床 done 迟到达后仍被正确消费 | job 消费 M3600 **恰好 1 次**,不因迟到丢失、不重复推进 | | b. 全检 done 提前(陈旧) | 启动 ~5s 注入一次 **M2098=1**(此时无 job 处于 OP40_REQ) | 全检模块陈旧信号守卫:`jobsAtStep(OP40_REQ)` 为空 → **忽略**(不广播、不推进) | 陈旧 M2098 被过滤,不误唤醒任何 job | | c. 两信号交叉到达 | M3600(迟到 10s)与真实 M2098 到达时间重叠 | doneWaits 按**信号名**匹配(单机器人同一时刻至多一个在途任务);M2098 仅在**发过 M2102** 后消费(批次广播与触发请求严格配对) | 各 job 消费各自的 done;M2098 消费不早于本批 M2102 发送;无错配 | **时序图(子场景 a+c 示意)**: ``` job5 磨削中 ──────── M3600(迟到10s) ──► 磨床 DONE → 换料 job6 放全检 ── M2109 ──► M2102 请求检验 ──► M2098 批次广播 ──► 全检槽 DONE (两信号在 ~10s 窗口内交叉到达,互不干扰) ``` **关键机制**: 1. **上升沿检测**:信号从 0→1 才算一次事件(持续为 1 不重复触发) 2. **doneWaits 登记先行**:派发前先登记 done 等待,防完成信号先于登记到达(约束 6.4.1) 3. **陈旧守卫**:M2098 只在有 job 处于 OP40_REQ(已发 M2102)时消费;M3600 只在有 OCCUPIED 槽时消费 4. **单在途任务**:机器人同一时刻只有一个在途任务,done 信号天然串行匹配 --- ## 七、关键交互场景状态流转 ### 7.1 磨床换料链(continuous_exchange,4 件 A/B/C/D 预置) ``` A(持毛坯,OP15) ──换B──► 磨床 OCCUPIED(A) [B 成品入夹爪] B(持成品) ──清洗──► 全检换料 ──► B 成品下料(OP60_EXCHANGE_LOAD) B ──OP61_DECIDE 决策:缓存有 C → 代取 C(OP50_EXCHANGE_TAKE) B(持 C) ──二次换料(OP40_EXCHANGE_2ND, proxy C)──► 全检 done 槽换主人 → D 成品入夹爪 D 下料(OP60_LOAD_2ND) ──► D COMPLETED;B 完成链 → 批次决策 OP40_BATCH_DECIDE ``` - ProxyJobID:代做链中缓存件 C 的槽位归属(预约/落位都按 ProxyJobID) - 断言:M2102 批次请求**恰好 1 次**;换料链 done 槽位不被自取任务抢走 (hasActiveExchangeChain 保护) ### 7.2 持料模型(airblow_hold) - 清洗期间 robot 被本 job 持有(temp_slot 不释放)→ 其他 job 的取料任务预约失败留队 - **唯一释放点**:放全检(U7 完成,M2109 到达后 temp_slot release) - 断言:temp_slot 释放审计事件仅落料点 1 次、清洗完成 0 次;其他 job 清洗晚于本 job 放全检 ### 7.3 槽位归还(dock_slot_return,6 件跨 2 接驳台) - 自行下料:成品回到**自己取料槽位**(dock_equipment_id 绑定) - 代下料(换料收尾件):回到 **PrevJobID 绑定槽位**(成品从哪来回哪去) - 断言:接驳台 OCCUPIED 槽数守恒(每件成品恰好一个落位) ### 7.4 缓存回流(cache_reflow) - 全检满 → 缓存暂存(U10,temp_slot_no 记录) - 全检空 → 回流(U14:取缓存+放全检连续动作,一次任务完成) - 断言:OP50_LOAD 暂存 → OP50_UNLOAD 回流步骤序列正确;temp_slot_no 清除 --- ## 八、执行方式与验收标准 ```bash # 全量 14 场景回归(每场景独立 DB 环境) go run ./cmd/mockrun/ -scenario [-jobs N] [-timeout T] [-verify] ``` 验收标准:每场景 `验证结果: N 通过, 0 失败`、exit=0、无 `level=ERROR` 日志。 场景与验证项见 `14个场景.md`;本文档为信号级流转依据。