# 换盘任务批次指令与双流向设计
## 概述
对换盘任务(ChangePallet)的取货/放货指令格式进行升级,支持批次编号和总数指令,并根据 `RobotSourceAddressLineCode` 区分两种流向。
## 背景
当前换盘任务实现(上一轮迭代)仅处理了 `HandlePutFinishedStateAsync` 中的假电芯补充逻辑,指令格式为简单的 `Pickbattery,{地址}` 和 `Pickbattery,5,{start}-{end}`。现需要:
1. 所有换盘取货/放货指令统一为批次格式 `{Command},{位置},{start}-{end}`
2. 每批前发送总数指令 `PickTotalNum,{N}` / `PutTotalNum,{N}`
3. 根据源地址线体编码区分两种完全不同的操作流向
## 指令格式
### 批次指令
| 指令类型 | 格式 | 示例 |
|----------|------|------|
| 取货总数 | `PickTotalNum,{N}` | `PickTotalNum,11` |
| 放货总数 | `PutTotalNum,{N}` | `PutTotalNum,11` |
| 取货批次 | `Pickbattery,{位置},{start}-{end}` | `Pickbattery,3,1-4` |
| 放货批次 | `Putbattery,{位置},{start}-{end}` | `Putbattery,6,5-8` |
| 取货单个 | `Pickbattery,{位置},{n}-0` | `Pickbattery,3,11-0` |
| 放货单个 | `Putbattery,{位置},{n}-0` | `Putbattery,6,11-0` |
- `PickTotalNum/PutTotalNum` 仅换盘任务发送,每批取/放之前都发
- `N` = 真实电芯数量(即 `task.RobotTaskTotalNum`),机器人固件用此值判断托盘上正常电芯总数,两种流向均发送相同的 N 值
- 每批最多4个,不满4个按实际数发,剩1个时 end=0
### 批次编号计算
`BuildBatchRange(currentIndex, remaining)` → `(start, end)`:
- `remaining >= 4` → `(currentIndex, currentIndex + 3)`
- `remaining > 1` → `(currentIndex, currentIndex + remaining - 1)`
- `remaining == 1` → `(currentIndex, 0)`
示例(targetNormalCount=11):第1批 1-4,第2批 5-8,第3批 9-11
示例(targetNormalCount=9):第1批 1-4,第2批 5-8,第3批 9-0(单个,end=0)
## 两种流向
### 流向A:补假电芯到目标托盘
**条件:** `RobotSourceAddressLineCode == "11001" || "11010"`
**场景:** 目标托盘有 N 个正常电芯(N < 48),需从5号位假电芯托盘取假电芯补满48个。
**阶段流转:**
```
[取假电芯] Pickbattery,5,{positionIndex} ← 从5号位取,编号用平面点位表PositionIndex
↓
[放假电芯] Putbattery,{目标地址},{N+1}-{N+4} ← 放到目标托盘,编号从正常数+1递增
↓
重复直到补满48个
```
假电芯数量 = `48 - task.RobotTaskTotalNum`
### 流向B:取正常电芯 + 回收假电芯
**条件:** `RobotSourceAddressLineCode != "11001" && != "11010"`
**场景:** 源托盘原本满48个(正常电芯 + 假电芯混合),先取走正常电芯放到目标托盘,再把源托盘上剩余的假电芯取出放回5号位。
**阶段流转:**
```
Phase 1 - 取正常电芯:
[取正常] Pickbattery,{源地址},{1}-{4} ← 编号从1递增
↓
[放正常] Putbattery,{目标地址},{1}-{4} ← 编号从1递增
↓
重复直到 N 个正常电芯全部取完
Phase 2 - 回收假电芯:
[取假电芯] Pickbattery,{源地址},{N+1}-{N+4} ← 编号从正常数+1继续递增
↓
[放假电芯] Putbattery,5,{positionIndex} ← 放回5号位,编号用平面点位表PositionIndex
↓
重复直到假电芯全部回收(48-N 个)
```
## 状态管理
### RobotSocketState 新增字段
```csharp
///
/// 当前批次起始编号(用于递增计算取货/放货编号)
///
public int CurrentBatchIndex { get; set; } = 1;
///
/// 换盘任务当前阶段
///
///
/// 0: 未开始
/// 1: 取正常电芯 / 取假电芯(流向A)
/// 2: 放正常电芯 / 放假电芯(流向A)
/// 3: 取假电芯(流向B Phase2)
/// 4: 放假电芯到5号位(流向B Phase2)
///
public int ChangePalletPhase { get; set; }
```
### CurrentBatchIndex 生命周期
**流向A:**
- Phase 0→1:`CurrentBatchIndex = 1`(初始化,用于 PositionIndex 查询起点)
- Phase 1→2:不重置(放货编号从 `targetNormalCount + 1` 开始,由 Orchestrator 计算)
- Phase 2→1:`CurrentBatchIndex += 本批数量`(递增,下一批继续)
- 完成→0:`CurrentBatchIndex = 1`(重置)
**流向B:**
- Phase 0→1:`CurrentBatchIndex = 1`(取正常电芯从1开始)
- Phase 1→2:不重置(放货编号与取货编号一致)
- Phase 2→1:`CurrentBatchIndex += 本批数量`(递增)
- Phase 2→3(正常电芯取完,即 `state.RobotTaskTotalNum >= targetNormalCount` 时 putfinished 完成后触发):`CurrentBatchIndex = targetNormalCount + 1`(假电芯从正常数+1开始)
- Phase 3→4:不重置(放假电芯用 PositionIndex,由 Orchestrator 计算)
- Phase 4→3:`CurrentBatchIndex += 本批数量`(递增)
- 完成→0:`CurrentBatchIndex = 1`(重置)
### 阶段流转状态机
**流向A:**
```
Phase 0 → Phase 1(取假电芯from 5号位)→ Phase 2(放假电芯to 目标)→ Phase 1 → ... → Phase 0(完成)
```
**流向B:**
```
Phase 0 → Phase 1(取正常from 源)→ Phase 2(放正常to 目标)→ Phase 1 → ...
→ Phase 3(取假电芯from 源)→ Phase 4(放假电芯to 5号位)→ Phase 3 → ... → Phase 0(完成)
```
## 代码改动范围
| 文件 | 改动 |
|------|------|
| `RobotSocketState.cs` | +`CurrentBatchIndex`, +`ChangePalletPhase` |
| `RobotTaskProcessor.cs` | +`BuildBatchRange()`, +`SendPickWithBatchAsync()`, +`SendPutWithBatchAsync()`, 修改现有假电芯方法 |
| `RobotWorkflowOrchestrator.cs` | 重写 ChangePallet 分支(HandlePutFinishedStateAsync + HandlePickFinishedStateAsync) |
| `RobotPrefixCommandHandler.cs` | HandlePutFinishedAsync 中区分换盘阶段:假电芯放货不调用 ChangePalletAsync API,不递增 `RobotTaskTotalNum`;HandlePickFinishedAsync 中假电芯取货不调用拆盘 API |
| `RobotSimpleCommandHandler.cs` | allpickfinished/allputfinished 中增加 `ChangePalletPhase` 守卫:仅当 Phase==0(所有阶段完成)时才触发入库和删除任务,中间阶段不处理 |
| `IFakeBatteryPositionService.cs` | +`MarkAsAvailable(List positions)` 方法 |
| `FakeBatteryPositionService.cs` | +`MarkAsAvailable` 实现 |
| `IFakeBatteryPositionRepository.cs` | +`MarkAsAvailable(List positions)` 方法 |
| `FakeBatteryPositionRepository.cs` | +`MarkAsAvailable` 实现(将指定点位 IsUsed 设为 false) |
## 命令处理器交互
### RobotPrefixCommandHandler 变更
**pickfinished 处理:**
- 当 `ChangePalletPhase == 3`(流向B取假电芯)时,不调用拆盘 API,仅更新状态
- 其他阶段保持现有逻辑
**putfinished 处理:**
- 判断流向:通过 `state.CurrentTask.RobotSourceAddressLineCode` 判断是流向A还是流向B
- 当 `ChangePalletPhase == 2` 且流向B(放正常电芯)时,正常调用 ChangePalletAsync API,`state.RobotTaskTotalNum += positions.Length`
- 当 `ChangePalletPhase == 4`(流向B放假电芯到5号位)时,不调用 API,不递增 `RobotTaskTotalNum`,调用 `MarkAsAvailable(positions)` 释放点位
- 当 `ChangePalletPhase == 2` 且流向A(放假电芯到目标)时,不调用 API,不递增 `RobotTaskTotalNum`
### RobotSimpleCommandHandler 变更
**allpickfinished / allputfinished:**
- 增加守卫条件:仅当 `state.ChangePalletPhase == 0`(所有阶段完成)时,才执行入库回传和任务删除
- 中间阶段收到 allpickfinished/allputfinished 时,仅更新 `state.CurrentAction`,不触发入库逻辑
- 任务完成时额外重置:`state.ChangePalletPhase = 0`, `state.CurrentBatchIndex = 1`, `state.IsInFakeBatteryMode = false`
## 边界条件
- `task.RobotTaskTotalNum == 48`:直接走原有逻辑,不进入批次模式
- `task.RobotTaskTotalNum == 0`:流向A 补满48个假电芯;流向B 跳过 Phase 1/2,直接进入 Phase 3/4 回收48个假电芯
- 假电芯平面点位不足:记录错误日志,中止当前批次
- 假电芯点位碎片化:`GetNextAvailable` 要求同行连续,如果请求4个找不到,依次尝试3、2、1个
- 批次编号溢出(超过48):不应发生,但需防御性检查