|
|
@@ -0,0 +1,784 @@
|
|
|
+# 舱室故障隔离与双端故障提示 实现计划
|
|
|
+
|
|
|
+> **给执行者(子代理/续接会话):** 必读子技能 —— 用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐 Task 执行。每步用 `- [ ]` 复选框跟踪。
|
|
|
+> 设计依据:`需求文档/specs/2026-06-23-舱室故障隔离与双端故障提示-design.md`(逐行源码核实的现状 + 目标 + 验收)。
|
|
|
+> **源码已用 codegraph 读透**(StartMain.InitTL/InitHouse、control 版 SerialBin 四个 errorlist 来源、MonitorSnapshot、告警通道 `/api/tl/control/alarm/reportCloudAlarm`),本计划所有 file:line 与签名均照此对齐。
|
|
|
+
|
|
|
+**目标(一句话):** 开机时部分舱坏 → 只排除坏舱、好舱照常培养、整机绝不因单舱失败而中止;并把"哪个舱/什么故障/何时/是否已隔离"经现有告警通道送到 operate + front 双端明确展示。
|
|
|
+
|
|
|
+**架构(怎么改):** control 侧把"一刀切 errorlist 非空即整体中止"改成"结构化坏舱清单 → 剔除坏舱 → 仅在零可跑舱时才中止";坏舱清单贯穿 `/status` 快照(operate 监控页轮询)+ 复用 `reportCloudAlarm` 告警(front 展示)。纯决策逻辑(坏舱剔除/致命判定)抽成无 I/O 静态类做 TDD;改启动核心(SerialBin/InitTL/InitHouse)的部分单测覆盖决策、行为靠真机拔插验收。
|
|
|
+
|
|
|
+**技术栈:** C# net6.0-windows;xUnit(复用现有 `IvfTl.ControlHost.Tests` 工程,不新建);现有 `HttpService.reportCloudAlarm` 告警通道 + `MonitorSnapshot` 快照;无新增第三方依赖。
|
|
|
+
|
|
|
+**分支:** `feature/house-fault-isolation`(独立于 D2-02;改 control 启动核心,风险较高,单独分支)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构(改面地图,决策先锁这里)
|
|
|
+
|
|
|
+**新增(纯数据/纯逻辑,可单测):**
|
|
|
+- `ivf_tl_operate_2.0/control/IvfTl.Control.Entity/InitEntitys/HouseFault.cs` — 坏舱故障数据载体 + `HouseFaultType` 枚举。放 Entity 工程,**ivf_tl_Com(SerialBin)与 ivf_tl_Control(StartMain/AppData/MonitorSnapshot)都引用得到**(二者均已 `using IvfTl.Control.Entity.*`)。
|
|
|
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.cs` — 无 I/O 静态决策类(坏舱集合/可跑舱/致命判定)。放 ivf_tl_Control,测试工程引用得到。
|
|
|
+
|
|
|
+**修改(改启动核心,真机门控):**
|
|
|
+- `ivf_tl_operate_2.0/control/ivf_tl_Com/SerialBin.cs` — 加 `public List<HouseFault> Faults`,在现有 4 处 `errorlist.Add` 旁同步登记结构化故障(保留 errorlist 供日志兼容)。
|
|
|
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs` — `InitTL` 用 `StartupFaultPolicy` 算可跑舱、仅零可跑舱才中止;`InitHouse` 逐舱构造/StartTask 各自 try-catch。
|
|
|
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs` — 加 `List<HouseFault> StartupFaults`,`GetMonitorSnapshot` 透出,启动排除时经 `HttpService` 上报告警。
|
|
|
+- `ivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.cs` — 加 `List<HouseFaultRow> Faults` 字段。
|
|
|
+
|
|
|
+**新增测试(并入现有工程):**
|
|
|
+- `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/HouseFaultTests.cs`
|
|
|
+- `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs`
|
|
|
+- `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 阶段划分(对齐 spec §59:①②control 侧纯单测+真机可自主,③④双端 UI 真机门控)
|
|
|
+
|
|
|
+- **第一阶段(本计划详细 Task1-7)** = control 坏舱剔除 + 故障登记 + 启动排除上报。Task1/2/6a 纯 TDD 自主;Task3/4/5/6b/7 改启动核心 + 真机拔插,Claude 可自主跑(UAC 提权、当前无活体培养)。
|
|
|
+- **第二/三/四/五阶段(本计划末尾提纲)** = 运行期故障升级去抖 / operate 监控页"舱故障"区 / front 告警展示 / 真机拔插整体验收。UI 两项真机门控,待第一阶段落地后单独拆细。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 第一阶段:control 坏舱剔除 + 故障登记 + 上报
|
|
|
+
|
|
|
+## Task 1: HouseFault 数据载体 + 枚举
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `ivf_tl_operate_2.0/control/IvfTl.Control.Entity/InitEntitys/HouseFault.cs`
|
|
|
+- Test: `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/HouseFaultTests.cs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+`IvfTl.ControlHost.Tests/HouseFaultTests.cs`:
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using IvfTl.Control.Entity.InitEntitys;
|
|
|
+using Xunit;
|
|
|
+
|
|
|
+namespace IvfTl.ControlHost.Tests
|
|
|
+{
|
|
|
+ public class HouseFaultTests
|
|
|
+ {
|
|
|
+ [Fact]
|
|
|
+ public void HouseFault_Holds_All_Fields()
|
|
|
+ {
|
|
|
+ var at = new DateTime(2026, 6, 23, 12, 0, 0, DateTimeKind.Utc);
|
|
|
+ var f = new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = 6,
|
|
|
+ Port = "COM7",
|
|
|
+ Type = HouseFaultType.CcdSnMissing,
|
|
|
+ Reason = "相机列表中不存在仓室的CCDSN12345",
|
|
|
+ Stage = "扫口握手",
|
|
|
+ At = at,
|
|
|
+ Isolated = true
|
|
|
+ };
|
|
|
+
|
|
|
+ Assert.Equal(6, f.HouseSn);
|
|
|
+ Assert.Equal("COM7", f.Port);
|
|
|
+ Assert.Equal(HouseFaultType.CcdSnMissing, f.Type);
|
|
|
+ Assert.Equal("相机列表中不存在仓室的CCDSN12345", f.Reason);
|
|
|
+ Assert.Equal("扫口握手", f.Stage);
|
|
|
+ Assert.Equal(at, f.At);
|
|
|
+ Assert.True(f.Isolated);
|
|
|
+ }
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void HouseFault_Defaults_HouseSn_Unknown_As_Minus1()
|
|
|
+ {
|
|
|
+ var f = new HouseFault { Type = HouseFaultType.CameraDuplicateSn };
|
|
|
+ Assert.Equal(-1, f.HouseSn); // -1 = 舱号未知(相机/串口级故障)
|
|
|
+ Assert.False(f.Isolated);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测试确认失败**
|
|
|
+
|
|
|
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter HouseFaultTests`
|
|
|
+Expected: FAIL —— 编译错误 `HouseFault`/`HouseFaultType` 未定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 写最小实现**
|
|
|
+
|
|
|
+`IvfTl.Control.Entity/InitEntitys/HouseFault.cs`:
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+
|
|
|
+namespace IvfTl.Control.Entity.InitEntitys
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// 舱级故障类型。启动期 SerialBin 登记 + 运行期升级共用。
|
|
|
+ /// </summary>
|
|
|
+ public enum HouseFaultType
|
|
|
+ {
|
|
|
+ CameraReadFailed = 0, // 相机 SN 读取异常(舱号未知)
|
|
|
+ CameraDuplicateSn = 1, // 两台相机报同一 SN,配对不明(舱号未知)
|
|
|
+ HouseSnDuplicate = 2, // 两个串口握手返回同一舱号
|
|
|
+ CcdSnMissing = 3, // 相机列表中不存在该舱 CCDSN(该舱相机半坏)
|
|
|
+ CcdSnDuplicate = 4, // 两个舱报同一 CCDSN
|
|
|
+ SerialReadException = 5, // 扫口过程串口读异常(舱号可能未知)
|
|
|
+ InitException = 6, // 舱构造/StartTask 抛异常
|
|
|
+ RuntimeFault = 7 // 运行期突发(第二阶段用)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 单条舱故障。HouseSn>0=已知具体舱(可被剔除);HouseSn=-1=相机/串口级故障(仅提示,不剔除某舱)。
|
|
|
+ /// 纯数据载体,无任何行为。
|
|
|
+ /// </summary>
|
|
|
+ public class HouseFault
|
|
|
+ {
|
|
|
+ /// <summary>舱号;-1 表示舱号未知(相机/串口级)。</summary>
|
|
|
+ public int HouseSn { get; set; } = -1;
|
|
|
+ /// <summary>串口名(已知时)。</summary>
|
|
|
+ public string Port { get; set; }
|
|
|
+ public HouseFaultType Type { get; set; }
|
|
|
+ /// <summary>人类可读原因(直接取自原 errorlist 文案)。</summary>
|
|
|
+ public string Reason { get; set; }
|
|
|
+ /// <summary>发生阶段:相机枚举/扫口握手/舱初始化/运行期。</summary>
|
|
|
+ public string Stage { get; set; }
|
|
|
+ /// <summary>发生时刻(UTC)。</summary>
|
|
|
+ public DateTime At { get; set; }
|
|
|
+ /// <summary>是否已被隔离(该舱已从可跑清单剔除/已停)。</summary>
|
|
|
+ public bool Isolated { get; set; }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter HouseFaultTests`
|
|
|
+Expected: PASS(2 绿)。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add ivf_tl_operate_2.0/control/IvfTl.Control.Entity/InitEntitys/HouseFault.cs \
|
|
|
+ ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/HouseFaultTests.cs
|
|
|
+git commit -m "feat(house-fault): HouseFault 数据载体 + HouseFaultType 枚举(TDD)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 2: StartupFaultPolicy 纯决策(坏舱剔除 / 致命判定)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.cs`
|
|
|
+- Test: `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs`
|
|
|
+
|
|
|
+**决策规则(锁定):** ① 坏舱 = 故障清单里 `HouseSn>0` 的舱号集合(舱号未知的相机/串口级故障**不剔除任何舱**,只作提示);② 可跑舱 = 发现的舱 − 坏舱;③ 致命 = **一个可跑舱都没有**(零可跑)才整机中止。单舱/部分舱坏一律不中止 —— 直接落实 spec §34「仅全部舱失败才中止」。
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**(完整真值表)
|
|
|
+
|
|
|
+`IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs`:
|
|
|
+```csharp
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using IvfTl.Control.Entity.InitEntitys;
|
|
|
+using ivf_tl_Control;
|
|
|
+using Xunit;
|
|
|
+
|
|
|
+namespace IvfTl.ControlHost.Tests
|
|
|
+{
|
|
|
+ public class StartupFaultPolicyTests
|
|
|
+ {
|
|
|
+ private static HouseFault Fault(int sn, HouseFaultType t = HouseFaultType.CcdSnMissing)
|
|
|
+ => new HouseFault { HouseSn = sn, Type = t, Reason = "x", Stage = "扫口握手" };
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void BadHouseSns_Only_Counts_Known_Houses()
|
|
|
+ {
|
|
|
+ var faults = new List<HouseFault>
|
|
|
+ {
|
|
|
+ Fault(6),
|
|
|
+ Fault(-1, HouseFaultType.CameraDuplicateSn), // 舱号未知 → 不算坏舱
|
|
|
+ Fault(8),
|
|
|
+ };
|
|
|
+ var bad = StartupFaultPolicy.BadHouseSns(faults);
|
|
|
+ Assert.Equal(new[] { 6, 8 }, bad.OrderBy(x => x).ToArray());
|
|
|
+ }
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void RunnableHouses_Excludes_Bad_And_Sorts()
|
|
|
+ {
|
|
|
+ var discovered = new[] { 2, 4, 6, 7, 8, 9, 11 };
|
|
|
+ var faults = new List<HouseFault> { Fault(6), Fault(8) };
|
|
|
+ var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
|
|
|
+ Assert.Equal(new[] { 2, 4, 7, 9, 11 }, run.ToArray());
|
|
|
+ }
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void RunnableHouses_With_No_Faults_Returns_All_Discovered()
|
|
|
+ {
|
|
|
+ var discovered = new[] { 2, 4, 6 };
|
|
|
+ var run = StartupFaultPolicy.RunnableHouses(discovered, new List<HouseFault>());
|
|
|
+ Assert.Equal(new[] { 2, 4, 6 }, run.ToArray());
|
|
|
+ }
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void RunnableHouses_UnknownHouseFault_Does_Not_Exclude_Anyone()
|
|
|
+ {
|
|
|
+ var discovered = new[] { 2, 4 };
|
|
|
+ var faults = new List<HouseFault> { Fault(-1, HouseFaultType.CameraReadFailed) };
|
|
|
+ var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
|
|
|
+ Assert.Equal(new[] { 2, 4 }, run.ToArray());
|
|
|
+ }
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void IsFatal_True_Only_When_No_Runnable_House()
|
|
|
+ {
|
|
|
+ Assert.True(StartupFaultPolicy.IsFatal(new List<int>()));
|
|
|
+ Assert.False(StartupFaultPolicy.IsFatal(new List<int> { 11 }));
|
|
|
+ Assert.False(StartupFaultPolicy.IsFatal(new List<int> { 2, 4 }));
|
|
|
+ }
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void Partial_Failure_Is_Never_Fatal()
|
|
|
+ {
|
|
|
+ // 7 个舱里坏 2 个 → 仍 5 个可跑 → 不致命(核心需求)
|
|
|
+ var discovered = new[] { 2, 4, 6, 7, 8, 9, 11 };
|
|
|
+ var faults = new List<HouseFault> { Fault(6), Fault(8) };
|
|
|
+ var run = StartupFaultPolicy.RunnableHouses(discovered, faults);
|
|
|
+ Assert.False(StartupFaultPolicy.IsFatal(run));
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测试确认失败**
|
|
|
+
|
|
|
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter StartupFaultPolicyTests`
|
|
|
+Expected: FAIL —— `StartupFaultPolicy` 未定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 写最小实现**
|
|
|
+
|
|
|
+`ivf_tl_Control/StartupFaultPolicy.cs`:
|
|
|
+```csharp
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using IvfTl.Control.Entity.InitEntitys;
|
|
|
+
|
|
|
+namespace ivf_tl_Control
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// 启动期坏舱决策(无 I/O,纯函数,可单测)。
|
|
|
+ /// 规则:坏舱=故障清单中已知舱号(HouseSn>0);可跑舱=发现的舱−坏舱;致命=零可跑舱。
|
|
|
+ /// 单舱/部分舱坏绝不致命 —— 落实 spec §34。
|
|
|
+ /// </summary>
|
|
|
+ public static class StartupFaultPolicy
|
|
|
+ {
|
|
|
+ public static HashSet<int> BadHouseSns(IEnumerable<HouseFault> faults)
|
|
|
+ => faults.Where(f => f.HouseSn > 0).Select(f => f.HouseSn).ToHashSet();
|
|
|
+
|
|
|
+ public static List<int> RunnableHouses(IEnumerable<int> discovered, IEnumerable<HouseFault> faults)
|
|
|
+ {
|
|
|
+ var bad = BadHouseSns(faults);
|
|
|
+ return discovered.Where(sn => !bad.Contains(sn)).Distinct().OrderBy(x => x).ToList();
|
|
|
+ }
|
|
|
+
|
|
|
+ public static bool IsFatal(IReadOnlyList<int> runnableHouses)
|
|
|
+ => runnableHouses == null || runnableHouses.Count == 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter StartupFaultPolicyTests`
|
|
|
+Expected: PASS(6 绿)。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.cs \
|
|
|
+ ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs
|
|
|
+git commit -m "feat(house-fault): StartupFaultPolicy 坏舱剔除/致命判定(TDD 真值表 6 绿)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 3: SerialBin 登记结构化坏舱清单(改启动核心,真机门控)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Com/SerialBin.cs`
|
|
|
+
|
|
|
+**说明:** control 版 SerialBin 现有 4 处 `errorlist.Add`(camera 重复 SN `:121`、camera 读异常 catch `:135`、舱号重复 `:212`、CCDSN 缺失 `:252`、CCDSN 重复 `:260`、扫口 catch `:318`)。在每处旁同步登记一条 `HouseFault`(**保留 errorlist 不动**,日志兼容)。纯增量,不改任何控制流。无法纯单测(真硬件 I/O),靠 Task 7 真机拔插验证。
|
|
|
+
|
|
|
+- [ ] **Step 1: 加 Faults 字段 + using**
|
|
|
+
|
|
|
+文件顶部已有 `using IvfTl.Control.Entity.InitEntitys;`(`HouseInitData`/`HouseEEPROInfo` 在此命名空间),`HouseFault` 同命名空间,无需加 using。在 `private List<string> errorlist = new List<string>();`(`SerialBin.cs:32`)下方加:
|
|
|
+```csharp
|
|
|
+ /// <summary>结构化坏舱清单(与 errorlist 并存,errorlist 仅日志兼容)。</summary>
|
|
|
+ public List<HouseFault> Faults { get; } = new List<HouseFault>();
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: camera 重复 SN(`GetCameraSn` 现 `:121`)**
|
|
|
+
|
|
|
+在 `errorlist.Add($"ccdid:{i};ccdsn:{SnNumber}");` 这一行**之后**追加(同一 `lock` 块内):
|
|
|
+```csharp
|
|
|
+ Faults.Add(new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = -1, Type = HouseFaultType.CameraDuplicateSn,
|
|
|
+ Reason = $"相机SN重复 ccdid:{i};ccdsn:{SnNumber}",
|
|
|
+ Stage = "相机枚举", At = DateTime.UtcNow
|
|
|
+ });
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: camera 读异常(`GetCameraSn` catch 现 `:135`)**
|
|
|
+
|
|
|
+在 `errorlist.Add($"获取{i}号相机ccdSn异常");` 之后追加(同 lock 块内):
|
|
|
+```csharp
|
|
|
+ Faults.Add(new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = -1, Type = HouseFaultType.CameraReadFailed,
|
|
|
+ Reason = $"获取{i}号相机ccdSn异常", Stage = "相机枚举", At = DateTime.UtcNow
|
|
|
+ });
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 舱号重复(`GetHouseInfo` 现 `:212`)**
|
|
|
+
|
|
|
+在 `errorlist.Add($"从{serialModel.housePort}串口...模块号{itemModel.houseSn}重复");` 之后追加(同 lock 块内,`goto CC;` 之前):
|
|
|
+```csharp
|
|
|
+ Faults.Add(new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = serialModel.houseSn, Port = serialModel.housePort,
|
|
|
+ Type = HouseFaultType.HouseSnDuplicate,
|
|
|
+ Reason = $"模块号{serialModel.houseSn}在{serialModel.housePort}与{itemModel.housePort}重复",
|
|
|
+ Stage = "扫口握手", At = DateTime.UtcNow
|
|
|
+ });
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: CCDSN 缺失(`GetHouseInfo` 现 `:252`)**
|
|
|
+
|
|
|
+在 `errorlist.Add($"相机列表中不存在仓室的CCDSN{currentCCDSN}");` 之后追加(同 lock 块内):
|
|
|
+```csharp
|
|
|
+ Faults.Add(new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = serialModel.houseSn, Port = serialModel.housePort,
|
|
|
+ Type = HouseFaultType.CcdSnMissing,
|
|
|
+ Reason = $"相机列表中不存在仓室的CCDSN{currentCCDSN}",
|
|
|
+ Stage = "扫口握手", At = DateTime.UtcNow
|
|
|
+ });
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: CCDSN 重复(`GetHouseInfo` 现 `:260`)**
|
|
|
+
|
|
|
+在 `errorlist.Add($"从{serialModel.houseSn}号模块...{itemModel.ccdSn}重复");` 之后追加(同 lock 块内):
|
|
|
+```csharp
|
|
|
+ Faults.Add(new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = serialModel.houseSn, Port = serialModel.housePort,
|
|
|
+ Type = HouseFaultType.CcdSnDuplicate,
|
|
|
+ Reason = $"舱{serialModel.houseSn}的CCDSN{currentCCDSN}与{itemModel.housePort}重复",
|
|
|
+ Stage = "扫口握手", At = DateTime.UtcNow
|
|
|
+ });
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 7: 扫口异常 catch(`GetHouseInfo` catch 现 `:318`)**
|
|
|
+
|
|
|
+在 `errorlist.Add($"获取端口信息异常{portName}:{ex.Message}");` 之后追加(同 lock 块内):
|
|
|
+```csharp
|
|
|
+ Faults.Add(new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = -1, Port = portName, Type = HouseFaultType.SerialReadException,
|
|
|
+ Reason = $"获取端口信息异常{portName}:{ex.Message}", Stage = "扫口握手", At = DateTime.UtcNow
|
|
|
+ });
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 8: 编译确认 0 错**
|
|
|
+
|
|
|
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug`
|
|
|
+Expected: 0 错(纯增量,Faults 只被写、还没被读)。
|
|
|
+
|
|
|
+- [ ] **Step 9: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add ivf_tl_operate_2.0/control/ivf_tl_Com/SerialBin.cs
|
|
|
+git commit -m "feat(house-fault): SerialBin 在 4 处 errorlist 旁登记结构化 HouseFault(保留 errorlist)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 4: StartMain.InitTL 改坏舱剔除(改启动核心,真机门控)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs`
|
|
|
+
|
|
|
+**说明:** 把"errorList 非空即整体 return 空"改为"算可跑舱、仅零可跑才中止、坏舱存 AppData"。InitTL 自身有真硬件 I/O 不纯单测;决策已被 Task 2 单测覆盖,这里只做接线,行为靠 Task 7 真机验证。
|
|
|
+
|
|
|
+- [ ] **Step 1: 加 using**
|
|
|
+
|
|
|
+`StartMain.cs` 顶部 `using IvfTl.Control.Entity.InitEntitys;` 已存在(用于 `TLInitData` 等),`HouseFault` 同命名空间可直接用,无需加 using。
|
|
|
+
|
|
|
+- [ ] **Step 2: 相机 errorlist 不再中止(现 `:78-83`)**
|
|
|
+
|
|
|
+把:
|
|
|
+```csharp
|
|
|
+ var errorList = serialBin.UpdataCamera();
|
|
|
+ AppData.LogService.TLLog($"相机信息:{JsonConvert.SerializeObject(serialBin.CCDidSn)}", LogEnum.RunRecord);
|
|
|
+ if (errorList.Any())
|
|
|
+ {
|
|
|
+ errora = $"获取相机Id和CCDSN错误{JsonConvert.SerializeObject(errorList)}";
|
|
|
+ AppData.LogService.TLLog(errora, LogEnum.RunRecord);
|
|
|
+ return (new TLInitControllerResult(), new List<int>());
|
|
|
+ }
|
|
|
+```
|
|
|
+改为(相机重复/读失败只记日志,不中止 —— 受影响舱稍后在 CCDSN 配对时自然落坏舱被剔除):
|
|
|
+```csharp
|
|
|
+ var errorList = serialBin.UpdataCamera();
|
|
|
+ AppData.LogService.TLLog($"相机信息:{JsonConvert.SerializeObject(serialBin.CCDidSn)}", LogEnum.RunRecord);
|
|
|
+ if (errorList.Any())
|
|
|
+ {
|
|
|
+ AppData.LogService.TLLog($"相机枚举有异常(不中止,受影响舱将按坏舱剔除):{JsonConvert.SerializeObject(errorList)}", LogEnum.RunRecord);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 串口 errorlist 改坏舱剔除(现 `:89-94`)**
|
|
|
+
|
|
|
+把:
|
|
|
+```csharp
|
|
|
+ if (errorList.Any())
|
|
|
+ {
|
|
|
+ errora = $"获取串口信息错误{JsonConvert.SerializeObject(errorList)}";
|
|
|
+ AppData.LogService.TLLog(errora, LogEnum.RunRecord);
|
|
|
+ return (new TLInitControllerResult(), new List<int>());
|
|
|
+ }
|
|
|
+```
|
|
|
+改为:
|
|
|
+```csharp
|
|
|
+ if (errorList.Any())
|
|
|
+ {
|
|
|
+ AppData.LogService.TLLog($"扫口有异常(将剔除坏舱后继续):{JsonConvert.SerializeObject(errorList)}", LogEnum.RunRecord);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 用 Policy 算可跑舱、零可跑才中止、坏舱存 AppData(现 `:120`)**
|
|
|
+
|
|
|
+把:
|
|
|
+```csharp
|
|
|
+ List<int> listIntRunHoues = serialBin.SerialModels.Select(x => x.houseSn).ToList();
|
|
|
+```
|
|
|
+改为:
|
|
|
+```csharp
|
|
|
+ var discovered = serialBin.SerialModels.Select(x => x.houseSn).ToList();
|
|
|
+ List<int> listIntRunHoues = StartupFaultPolicy.RunnableHouses(discovered, serialBin.Faults);
|
|
|
+
|
|
|
+ // 坏舱清单存 AppData(供 /status 快照透出 + 上报告警);已知舱标记已隔离。
|
|
|
+ foreach (var f in serialBin.Faults) if (f.HouseSn > 0) f.Isolated = true;
|
|
|
+ AppData.StartupFaults = serialBin.Faults;
|
|
|
+ if (serialBin.Faults.Any())
|
|
|
+ AppData.LogService.TLLog($"启动坏舱清单:{JsonConvert.SerializeObject(serialBin.Faults)};可跑舱:{string.Join(",", listIntRunHoues)}", LogEnum.RunRecord);
|
|
|
+
|
|
|
+ // 仅零可跑舱才整机中止(单舱/部分舱坏绝不中止 —— spec §34)。
|
|
|
+ if (StartupFaultPolicy.IsFatal(listIntRunHoues))
|
|
|
+ {
|
|
|
+ errora = "所有舱室初始化失败,无可运行舱室";
|
|
|
+ AppData.LogService.TLLog(errora, LogEnum.RunRecord);
|
|
|
+ return (new TLInitControllerResult(), new List<int>());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上报启动排除告警(每坏舱一条;失败不影响启动)。
|
|
|
+ try { AppData.ReportStartupFaults(); } catch { }
|
|
|
+```
|
|
|
+
|
|
|
+> 注:`AppData.StartupFaults` 与 `AppData.ReportStartupFaults()` 在 Task 6 实现;本 Task 先写调用,Task 6 补定义后整体编译通过。**执行顺序:Task 4 与 Task 6 同一提交前一起编译**(或先做 Task 6 再做 Task 4)。建议:**先做 Task 6 再回填 Task 4 的两处调用**,避免中途不编译。
|
|
|
+
|
|
|
+- [ ] **Step 5: 编译确认 0 错**(在 Task 6 完成后)
|
|
|
+
|
|
|
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug`
|
|
|
+Expected: 0 错。
|
|
|
+
|
|
|
+- [ ] **Step 6: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs
|
|
|
+git commit -m "feat(house-fault): InitTL 改坏舱剔除——零可跑才中止,坏舱存AppData+上报告警"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 5: InitHouse 逐舱 try-catch(单舱构造异常只跳过该舱,真机门控)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs`(`InitHouse` 现 `:160-259`)
|
|
|
+
|
|
|
+**说明:** 现 `InitHouse` 是一个大 try-catch —— 任一 `HouseBinN` 构造抛异常 → 整个 InitHouse return false → **所有舱都起不来**。改为每舱构造 + StartTask 各自 try-catch,单舱异常只记 `HouseFault{InitException}` 并跳过(spec §35)。10 个舱重复,用本地 helper DRY。
|
|
|
+
|
|
|
+- [ ] **Step 1: 在 InitHouse 内加本地 helper(方法体开头,`TLSetting tLSetting = ...` 之前)**
|
|
|
+
|
|
|
+```csharp
|
|
|
+ // 单舱构造/启动各自兜底:抛异常只登记坏舱+跳过,不拖垮其余舱。
|
|
|
+ void TryHouse(int sn, Action build, bool run)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ build();
|
|
|
+ if (run) { /* StartTask 在下方按 runHouses 统一触发,这里只构造 */ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ AppData.LogService.ExceptionLog(ex, $"舱{sn}构造失败(已隔离,其余舱继续)", null, LogEnum.RunException);
|
|
|
+ AppData.StartupFaults?.Add(new HouseFault
|
|
|
+ {
|
|
|
+ HouseSn = sn, Type = HouseFaultType.InitException,
|
|
|
+ Reason = $"舱{sn}构造异常:{ex.Message}", Stage = "舱初始化",
|
|
|
+ At = DateTime.UtcNow, Isolated = true
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+> 注:`using System;`(Action/DateTime)在 StartMain 顶部已隐式可用(已 `using System.Diagnostics;` 等;若 `Action` 报未定义,加 `using System;`)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 每舱构造包进 TryHouse**
|
|
|
+
|
|
|
+把现有每舱两行(以舱 2 为例)
|
|
|
+```csharp
|
|
|
+ housesn = 2;
|
|
|
+ AppData.HouseBin2 = new HouseBin(tLSetting, tLInitControllerResult.HouseList.FirstOrDefault(x => x.houseSn == housesn), allList.Dishes.FirstOrDefault(x => x.houseSn == housesn), allList.BalanceList.FirstOrDefault(x => x.houseSn == housesn), tLInitControllerResult.HouseWellList.Where(x => x.houseSn == housesn).ToList());
|
|
|
+ AppData.InitHouseBinEvent(AppData.HouseBin2);
|
|
|
+```
|
|
|
+改为:
|
|
|
+```csharp
|
|
|
+ housesn = 2;
|
|
|
+ TryHouse(2, () => {
|
|
|
+ AppData.HouseBin2 = new HouseBin(tLSetting, tLInitControllerResult.HouseList.FirstOrDefault(x => x.houseSn == 2), allList.Dishes.FirstOrDefault(x => x.houseSn == 2), allList.BalanceList.FirstOrDefault(x => x.houseSn == 2), tLInitControllerResult.HouseWellList.Where(x => x.houseSn == 2).ToList());
|
|
|
+ AppData.InitHouseBinEvent(AppData.HouseBin2);
|
|
|
+ }, false);
|
|
|
+```
|
|
|
+对舱 1~10 同样处理(缓冲瓶 BufferBottleBin/舱 11 沿用原写法,11 失败由 InitTL 的可跑舱判定兜底,不在此包裹)。`CamNum` 赋值段(现 `:215-224`)保持不变,但需对可能为 null 的 HouseBinN 加判空:`AppData.HouseBin2?.CamNum = CamNum;` 不合法 —— 改为 `if (AppData.HouseBin2 != null) AppData.HouseBin2.CamNum = CamNum;`(10 舱逐个)。
|
|
|
+
|
|
|
+- [ ] **Step 3: StartTask 段对 null 兜底(现 `:226-250`)**
|
|
|
+
|
|
|
+把每行 `if (runHouses.Contains(2)) AppData.HouseBin2.StartTask();` 改为:
|
|
|
+```csharp
|
|
|
+ if (runHouses.Contains(2) && AppData.HouseBin2 != null)
|
|
|
+ try { AppData.HouseBin2.StartTask(); }
|
|
|
+ catch (Exception ex) { AppData.LogService.ExceptionLog(ex, "舱2 StartTask 失败(已隔离)", null, LogEnum.RunException); }
|
|
|
+```
|
|
|
+对舱 1/3/4/5/6/7/8/9/10 与 BufferBottleBin(11)同样加 `!= null` 判空 + try-catch。
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译确认 0 错**
|
|
|
+
|
|
|
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug`
|
|
|
+Expected: 0 错。
|
|
|
+
|
|
|
+- [ ] **Step 5: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs
|
|
|
+git commit -m "feat(house-fault): InitHouse 逐舱 try-catch——单舱构造/启动异常只隔离该舱"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 6: AppData 坏舱清单字段 + 快照透出 + 告警上报
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs`
|
|
|
+- Modify: `ivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.cs`
|
|
|
+- Test: `ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs`
|
|
|
+
|
|
|
+### 6a. MonitorSnapshot 加 Faults 字段(纯数据,TDD)
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+`IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs`:
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using ivf_tl_Control;
|
|
|
+using Xunit;
|
|
|
+
|
|
|
+namespace IvfTl.ControlHost.Tests
|
|
|
+{
|
|
|
+ public class MonitorSnapshotFaultTests
|
|
|
+ {
|
|
|
+ [Fact]
|
|
|
+ public void Snapshot_Faults_Defaults_Empty_Not_Null()
|
|
|
+ {
|
|
|
+ var s = new MonitorSnapshot();
|
|
|
+ Assert.NotNull(s.Faults);
|
|
|
+ Assert.Empty(s.Faults);
|
|
|
+ }
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void HouseFaultRow_Holds_Fields()
|
|
|
+ {
|
|
|
+ var row = new HouseFaultRow
|
|
|
+ {
|
|
|
+ HouseSn = 6, FaultType = "CcdSnMissing",
|
|
|
+ Reason = "相机列表中不存在仓室的CCDSN12345",
|
|
|
+ Stage = "扫口握手", At = new DateTime(2026, 6, 23), Isolated = true
|
|
|
+ };
|
|
|
+ Assert.Equal(6, row.HouseSn);
|
|
|
+ Assert.Equal("CcdSnMissing", row.FaultType);
|
|
|
+ Assert.True(row.Isolated);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测试确认失败**
|
|
|
+
|
|
|
+Run: `dotnet test ...IvfTl.ControlHost.Tests.csproj --filter MonitorSnapshotFaultTests`
|
|
|
+Expected: FAIL —— `MonitorSnapshot.Faults`/`HouseFaultRow` 未定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现 —— MonitorSnapshot.cs 末尾(`MonitorSnapshot` 类内加字段 + 文件内加 `HouseFaultRow` 类)**
|
|
|
+
|
|
|
+在 `MonitorSnapshot` 类内 `Houses` 字段后加:
|
|
|
+```csharp
|
|
|
+ /// <summary>舱故障列表(启动排除 + 运行期突发),供 operate 监控页红色高亮展示。</summary>
|
|
|
+ public List<HouseFaultRow> Faults { get; set; } = new List<HouseFaultRow>();
|
|
|
+```
|
|
|
+在文件内 `HouseMonitorRow` 类后(命名空间内)加:
|
|
|
+```csharp
|
|
|
+ /// <summary>单条舱故障只读行(operate/front 展示用,字符串化枚举便于跨端)。</summary>
|
|
|
+ public class HouseFaultRow
|
|
|
+ {
|
|
|
+ public int HouseSn { get; set; }
|
|
|
+ public string FaultType { get; set; }
|
|
|
+ public string Reason { get; set; }
|
|
|
+ public string Stage { get; set; }
|
|
|
+ public DateTime At { get; set; }
|
|
|
+ public bool Isolated { get; set; }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测试确认通过**
|
|
|
+
|
|
|
+Run: `dotnet test ...IvfTl.ControlHost.Tests.csproj --filter MonitorSnapshotFaultTests`
|
|
|
+Expected: PASS(2 绿)。
|
|
|
+
|
|
|
+### 6b. AppData.StartupFaults + GetMonitorSnapshot 透出 + ReportStartupFaults
|
|
|
+
|
|
|
+- [ ] **Step 5: AppData 加 StartupFaults 字段**
|
|
|
+
|
|
|
+`AppData.cs` 在 `MqttService` 等属性附近(如 `CurrentUser` 属性后)加:
|
|
|
+```csharp
|
|
|
+ /// <summary>启动期坏舱清单(InitTL 写入,GetMonitorSnapshot 透出,ReportStartupFaults 上报)。</summary>
|
|
|
+ public List<HouseFault> StartupFaults { get; set; } = new List<HouseFault>();
|
|
|
+```
|
|
|
+确认文件顶部已 `using IvfTl.Control.Entity.InitEntitys;`(AppData 大量用 InitEntitys);若无则加。
|
|
|
+
|
|
|
+- [ ] **Step 6: GetMonitorSnapshot 透出 Faults(`AppData.cs:222` 附近,构造 `MonitorSnapshot` 处)**
|
|
|
+
|
|
|
+在 `GetMonitorSnapshot` 内、`new MonitorSnapshot{...}` 赋值各字段后(或返回前),把 StartupFaults 映射进快照:
|
|
|
+```csharp
|
|
|
+ // 舱故障透出(StartupFaults → HouseFaultRow,字符串化枚举跨端)。
|
|
|
+ try
|
|
|
+ {
|
|
|
+ snapshot.Faults = (StartupFaults ?? new List<HouseFault>())
|
|
|
+ .Select(f => new HouseFaultRow
|
|
|
+ {
|
|
|
+ HouseSn = f.HouseSn, FaultType = f.Type.ToString(),
|
|
|
+ Reason = f.Reason, Stage = f.Stage, At = f.At, Isolated = f.Isolated
|
|
|
+ }).ToList();
|
|
|
+ }
|
|
|
+ catch { }
|
|
|
+```
|
|
|
+> `snapshot` 为该方法内构造的 `MonitorSnapshot` 局部变量名;若现有代码用对象初始化器一次性 `return new MonitorSnapshot{...}`,改为先赋给局部 `var snapshot = new MonitorSnapshot{...};` 再补上面映射,最后 `return snapshot;`。需要 `using System.Linq;`(AppData 已用 Linq)。
|
|
|
+
|
|
|
+- [ ] **Step 7: AppData 加 ReportStartupFaults(复用现有告警通道)**
|
|
|
+
|
|
|
+参照现有 `MqttAlarm`(`AppData.cs:1350`,走 `HttpService.AlarmApi1`)与 `AlarmApi`(`HttpService.cs:54`,POST `/api/tl/control/alarm/reportCloudAlarm`,带 houseSn/wellSn)。新增:
|
|
|
+```csharp
|
|
|
+ /// <summary>
|
|
|
+ /// 上报启动期坏舱告警(每坏舱一条)。复用 HttpService.AlarmApi(reportCloudAlarm)。
|
|
|
+ /// alarmTypeKey 按故障类型映射;paramList 携带原因/阶段。全 try 兜底,失败不影响启动。
|
|
|
+ /// 注:新 alarmTypeKey 需在 Java 告警字典登记(否则落库被拒)—— 见计划「跨端依赖」。
|
|
|
+ /// </summary>
|
|
|
+ public void ReportStartupFaults()
|
|
|
+ {
|
|
|
+ if (StartupFaults == null || StartupFaults.Count == 0) return;
|
|
|
+ string tlSn = "";
|
|
|
+ try { tlSn = TLSetting?.tlSn ?? ""; } catch { }
|
|
|
+ foreach (var f in StartupFaults)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ string key = f.Type == HouseFaultType.CcdSnMissing || f.Type == HouseFaultType.CameraDuplicateSn || f.Type == HouseFaultType.CameraReadFailed
|
|
|
+ ? "HOUSE_CAMERA_FAULT"
|
|
|
+ : (f.Type == HouseFaultType.SerialReadException ? "HOUSE_SERIAL_FAULT" : "HOUSE_INIT_EXCLUDED");
|
|
|
+ HttpService.AlarmApi(tlSn, f.HouseSn > 0 ? f.HouseSn : 0, 0, key,
|
|
|
+ new List<string> { f.Type.ToString(), f.Reason, f.Stage });
|
|
|
+ }
|
|
|
+ catch (Exception ex) { ExLog(ex, "ReportStartupFaults"); }
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 8: 编译 + 跑全量单测**
|
|
|
+
|
|
|
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug` → 0 错
|
|
|
+Run: `dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj`
|
|
|
+Expected: 既有 27 + 新增(HouseFault 2 + Policy 6 + Snapshot 2)= **37 绿 0 失败**。
|
|
|
+
|
|
|
+- [ ] **Step 9: 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs \
|
|
|
+ ivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.cs \
|
|
|
+ ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs
|
|
|
+git commit -m "feat(house-fault): AppData.StartupFaults+快照透出+ReportStartupFaults;MonitorSnapshot.Faults(TDD 37绿)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 7: 真机拔插验收(Claude 自主,UAC 提权)
|
|
|
+
|
|
|
+**前置:** 当前无活体培养(拔插不伤生物);UAC 可静默提权起停 control(见记忆 [[control-realmachine-smoke-startup]]);control 须从 operate 输出根的 `control\` 子目录跑(`..\tl-shared.config` 才解析得到,见交接卡 2026-06-23 真机段第 4 点)。
|
|
|
+
|
|
|
+- [ ] **Step 1: 编译 Release + 部署**
|
|
|
+
|
|
|
+Run: `dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Release`(真机/连内网必须 Release);确认 `control\` 子目录布局 + `tl-shared.config` 在 operate 根。
|
|
|
+
|
|
|
+- [ ] **Step 2: 基线(全好舱)** —— 提权起 control,`/status` 看 `snapshot.Faults` 为空、`Houses` 含全部扫到的舱、`started:true`。记录可跑舱清单。
|
|
|
+
|
|
|
+- [ ] **Step 3: 制造一个"半坏"舱** —— 物理拔掉某有效舱的相机 USB(该舱串口仍活)→ 重启 control。预期:
|
|
|
+ - control **不中止**、`started:true`;
|
|
|
+ - 该舱出现在 `snapshot.Faults`(`FaultType=HOUSE_CAMERA_FAULT/CcdSnMissing`、`Isolated:true`),且**不在**可跑舱清单;
|
|
|
+ - **其余舱全部正常 StartAsync**(读温≈36℃、握手正常)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 告警落库核实** —— 用 JDBC(`临时文件/DbSql.java` 模式)连 108 查 `aivfo-tl-control` 告警表,确认该坏舱告警入库(alarmTypeKey + houseSn + 原因)。**若被拒**,记录:需 Java 告警字典登记新 alarmTypeKey(见「跨端依赖」),作为 Phase 4 门控项。
|
|
|
+
|
|
|
+- [ ] **Step 5: 恢复 + 复测** —— 插回相机重启 control,确认该舱回到可跑、`Faults` 不再含它。
|
|
|
+
|
|
|
+- [ ] **Step 6: 回写文档**(见回写协议)+ 提交。
|
|
|
+
|
|
|
+```bash
|
|
|
+git add 项目文档/进度/进度状态.yaml 项目文档/进度/交接卡.md 项目文档/进度/工作计划表.md 项目文档/进度/待验证清单.md 项目文档/进度/进度数据.js
|
|
|
+git commit -m "docs(house-fault): 第一阶段 control 坏舱剔除真机拔插验收回写"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 后续阶段(提纲,第一阶段落地后单独拆细)
|
|
|
+
|
|
|
+## 第二阶段:运行期故障升级 + 去抖(control,可自主)
|
|
|
+- `HouseBin/BufferBottleBin.MainThread`、`ComBin.StartSendCommandThread` 的 `catch` 里:同舱连续失败计数达阈值(去抖,避免每圈刷屏)→ 升级一条 `HouseFault{RuntimeFault}` 进 `AppData.StartupFaults`(改名/或新增 `RuntimeFaults`)+ `ReportStartupFaults` 同款上报 `HOUSE_RUNTIME_FAULT`;失败计数清零条件 = 该舱恢复一次成功通讯。
|
|
|
+- 纯逻辑(计数/阈值/去抖)可 TDD;升级动作真机验证(运行中拔串口/相机)。
|
|
|
+
|
|
|
+## 第三阶段:operate 监控页"舱故障"区(真机门控)
|
|
|
+- `ivf_tl_Operate` 监控页(读 `ControlClient` → `/status` 的 `snapshot.Faults`)新增"舱故障"区:红色高亮列出每故障舱(舱号/类型/时间/已隔离),与现有监控三块并列。
|
|
|
+- 复用现有轮询;UI 真机门控(需 operate 外壳跑起来,受僵尸 operate Mutex 影响,需先清)。
|
|
|
+
|
|
|
+## 第四阶段:front 告警展示(真机门控)+ 跨端依赖
|
|
|
+- **跨端依赖(必须先确认):** 新 alarmTypeKey(`HOUSE_INIT_EXCLUDED`/`HOUSE_CAMERA_FAULT`/`HOUSE_SERIAL_FAULT`/`HOUSE_RUNTIME_FAULT`)需在 Java 告警字典/告警类型配置登记,否则 `reportCloudAlarm` 落库被拒、front 也无从展示。Task 7 Step 4 已先探;此阶段落地登记。
|
|
|
+- front 舱位图/告警列表:故障舱标红 + 统一文案("X 号舱相机故障,已隔离,其余舱正常");接现有 alarm_data + MQTT 流。
|
|
|
+
|
|
|
+## 第五阶段:真机拔插整体验收(spec §48-51 三条)
|
|
|
+- ① 启动期半坏舱排除、其余正常;② 运行期拔串口/相机隔离、其余采集不中断;③ 双端(operate 监控页 + front)都明确显示故障舱(舱号/类型/时间)。
|
|
|
+- 逐条回写 `待验证清单.md`。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 自查(写完对照 spec)
|
|
|
+
|
|
|
+- **spec §27 启动期按舱容错** → Task 2(Policy 剔除)+ Task 3(SerialBin 登记)+ Task 4(InitTL 不再一刀切)+ Task 5(InitHouse 逐舱兜底)。✓
|
|
|
+- **spec §28 双端明确提示** → Task 6(快照 Faults + ReportStartupFaults 上报)覆盖 control 侧出口;operate/front 展示在第三/四阶段。✓(第一阶段做到"故障可被双端拿到",展示分阶段)
|
|
|
+- **spec §33-35 设计要点(区分致命/单舱、剔除坏舱、逐舱 try-catch)** → Task 2/3/4/5 一一对应。✓
|
|
|
+- **spec §38-41 统一上报(复用 alarm_data+MQTT、/status 加舱故障字段、运行期去抖)** → Task 6(复用 reportCloudAlarm + 快照字段)+ 第二阶段(去抖)。✓
|
|
|
+- **spec §48-51 真机验收** → Task 7(启动期)+ 第五阶段(运行期+双端)。✓
|
|
|
+- **类型一致性自查:** `HouseFault`(Entity)字段贯穿 SerialBin/InitTL/AppData;`HouseFaultRow`(MonitorSnapshot)与 AppData 映射字段名一致(HouseSn/FaultType/Reason/Stage/At/Isolated);`StartupFaultPolicy.RunnableHouses/IsFatal/BadHouseSns` 签名在 Task2 定义、Task4 调用一致;`AppData.StartupFaults`/`ReportStartupFaults` Task4 调用、Task6 定义一致(执行注:先做 Task6 再回填 Task4 调用,保证中途可编译)。✓
|
|
|
+- **占位扫描:** 无 TBD/TODO;每代码步均有完整代码块。真机行为类步骤(Task3/4/5/7)给出确切编辑点 file:line 与验证预期,非"自行处理"。✓
|