给执行者(子代理/续接会话): 必读子技能 —— 用 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.csivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.csivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.csFiles:
ivf_tl_operate_2.0/control/IvfTl.Control.Entity/InitEntitys/HouseFault.csTest: ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/HouseFaultTests.cs
[ ] Step 1: 写失败测试
IvfTl.ControlHost.Tests/HouseFaultTests.cs:
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);
}
}
}
Run: dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter HouseFaultTests
Expected: FAIL —— 编译错误 HouseFault/HouseFaultType 未定义。
IvfTl.Control.Entity/InitEntitys/HouseFault.cs:
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; }
}
}
Run: dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter HouseFaultTests
Expected: PASS(2 绿)。
[ ] Step 5: 提交
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)"
Files:
ivf_tl_operate_2.0/control/ivf_tl_Control/StartupFaultPolicy.csivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs决策规则(锁定): ① 坏舱 = 故障清单里 HouseSn>0 的舱号集合(舱号未知的相机/串口级故障不剔除任何舱,只作提示);② 可跑舱 = 发现的舱 − 坏舱;③ 致命 = 一个可跑舱都没有(零可跑)才整机中止。单舱/部分舱坏一律不中止 —— 直接落实 spec §34「仅全部舱失败才中止」。
IvfTl.ControlHost.Tests/StartupFaultPolicyTests.cs:
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));
}
}
}
Run: dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter StartupFaultPolicyTests
Expected: FAIL —— StartupFaultPolicy 未定义。
ivf_tl_Control/StartupFaultPolicy.cs:
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;
}
}
Run: dotnet test ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/IvfTl.ControlHost.Tests.csproj --filter StartupFaultPolicyTests
Expected: PASS(6 绿)。
[ ] Step 5: 提交
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 绿)"
Files:
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 真机拔插验证。
文件顶部已有 using IvfTl.Control.Entity.InitEntitys;(HouseInitData/HouseEEPROInfo 在此命名空间),HouseFault 同命名空间,无需加 using。在 private List<string> errorlist = new List<string>();(SerialBin.cs:32)下方加:
/// <summary>结构化坏舱清单(与 errorlist 并存,errorlist 仅日志兼容)。</summary>
public List<HouseFault> Faults { get; } = new List<HouseFault>();
GetCameraSn 现 :121)在 errorlist.Add($"ccdid:{i};ccdsn:{SnNumber}"); 这一行之后追加(同一 lock 块内):
Faults.Add(new HouseFault
{
HouseSn = -1, Type = HouseFaultType.CameraDuplicateSn,
Reason = $"相机SN重复 ccdid:{i};ccdsn:{SnNumber}",
Stage = "相机枚举", At = DateTime.UtcNow
});
GetCameraSn catch 现 :135)在 errorlist.Add($"获取{i}号相机ccdSn异常"); 之后追加(同 lock 块内):
Faults.Add(new HouseFault
{
HouseSn = -1, Type = HouseFaultType.CameraReadFailed,
Reason = $"获取{i}号相机ccdSn异常", Stage = "相机枚举", At = DateTime.UtcNow
});
GetHouseInfo 现 :212)在 errorlist.Add($"从{serialModel.housePort}串口...模块号{itemModel.houseSn}重复"); 之后追加(同 lock 块内,goto CC; 之前):
Faults.Add(new HouseFault
{
HouseSn = serialModel.houseSn, Port = serialModel.housePort,
Type = HouseFaultType.HouseSnDuplicate,
Reason = $"模块号{serialModel.houseSn}在{serialModel.housePort}与{itemModel.housePort}重复",
Stage = "扫口握手", At = DateTime.UtcNow
});
GetHouseInfo 现 :252)在 errorlist.Add($"相机列表中不存在仓室的CCDSN{currentCCDSN}"); 之后追加(同 lock 块内):
Faults.Add(new HouseFault
{
HouseSn = serialModel.houseSn, Port = serialModel.housePort,
Type = HouseFaultType.CcdSnMissing,
Reason = $"相机列表中不存在仓室的CCDSN{currentCCDSN}",
Stage = "扫口握手", At = DateTime.UtcNow
});
GetHouseInfo 现 :260)在 errorlist.Add($"从{serialModel.houseSn}号模块...{itemModel.ccdSn}重复"); 之后追加(同 lock 块内):
Faults.Add(new HouseFault
{
HouseSn = serialModel.houseSn, Port = serialModel.housePort,
Type = HouseFaultType.CcdSnDuplicate,
Reason = $"舱{serialModel.houseSn}的CCDSN{currentCCDSN}与{itemModel.housePort}重复",
Stage = "扫口握手", At = DateTime.UtcNow
});
GetHouseInfo catch 现 :318)在 errorlist.Add($"获取端口信息异常{portName}:{ex.Message}"); 之后追加(同 lock 块内):
Faults.Add(new HouseFault
{
HouseSn = -1, Port = portName, Type = HouseFaultType.SerialReadException,
Reason = $"获取端口信息异常{portName}:{ex.Message}", Stage = "扫口握手", At = DateTime.UtcNow
});
Run: dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug
Expected: 0 错(纯增量,Faults 只被写、还没被读)。
[ ] Step 9: 提交
git add ivf_tl_operate_2.0/control/ivf_tl_Com/SerialBin.cs
git commit -m "feat(house-fault): SerialBin 在 4 处 errorlist 旁登记结构化 HouseFault(保留 errorlist)"
Files:
ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs说明: 把"errorList 非空即整体 return 空"改为"算可跑舱、仅零可跑才中止、坏舱存 AppData"。InitTL 自身有真硬件 I/O 不纯单测;决策已被 Task 2 单测覆盖,这里只做接线,行为靠 Task 7 真机验证。
StartMain.cs 顶部 using IvfTl.Control.Entity.InitEntitys; 已存在(用于 TLInitData 等),HouseFault 同命名空间可直接用,无需加 using。
:78-83)把:
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 配对时自然落坏舱被剔除):
var errorList = serialBin.UpdataCamera();
AppData.LogService.TLLog($"相机信息:{JsonConvert.SerializeObject(serialBin.CCDidSn)}", LogEnum.RunRecord);
if (errorList.Any())
{
AppData.LogService.TLLog($"相机枚举有异常(不中止,受影响舱将按坏舱剔除):{JsonConvert.SerializeObject(errorList)}", LogEnum.RunRecord);
}
:89-94)把:
if (errorList.Any())
{
errora = $"获取串口信息错误{JsonConvert.SerializeObject(errorList)}";
AppData.LogService.TLLog(errora, LogEnum.RunRecord);
return (new TLInitControllerResult(), new List<int>());
}
改为:
if (errorList.Any())
{
AppData.LogService.TLLog($"扫口有异常(将剔除坏舱后继续):{JsonConvert.SerializeObject(errorList)}", LogEnum.RunRecord);
}
:120)把:
List<int> listIntRunHoues = serialBin.SerialModels.Select(x => x.houseSn).ToList();
改为:
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 的两处调用,避免中途不编译。
Run: dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug
Expected: 0 错。
[ ] Step 6: 提交
git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs
git commit -m "feat(house-fault): InitTL 改坏舱剔除——零可跑才中止,坏舱存AppData+上报告警"
Files:
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 = ... 之前)
// 单舱构造/启动各自兜底:抛异常只登记坏舱+跳过,不拖垮其余舱。
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;)。
把现有每舱两行(以舱 2 为例)
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);
改为:
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 舱逐个)。
:226-250)把每行 if (runHouses.Contains(2)) AppData.HouseBin2.StartTask(); 改为:
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。
Run: dotnet build ivf_tl_operate_2.0/control/ivf_tl_Control.sln -c Debug
Expected: 0 错。
[ ] Step 5: 提交
git add ivf_tl_operate_2.0/control/ivf_tl_Control/StartMain.cs
git commit -m "feat(house-fault): InitHouse 逐舱 try-catch——单舱构造/启动异常只隔离该舱"
Files:
ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.csivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.csivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.csIvfTl.ControlHost.Tests/MonitorSnapshotFaultTests.cs:
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);
}
}
}
Run: dotnet test ...IvfTl.ControlHost.Tests.csproj --filter MonitorSnapshotFaultTests
Expected: FAIL —— MonitorSnapshot.Faults/HouseFaultRow 未定义。
MonitorSnapshot 类内加字段 + 文件内加 HouseFaultRow 类)在 MonitorSnapshot 类内 Houses 字段后加:
/// <summary>舱故障列表(启动排除 + 运行期突发),供 operate 监控页红色高亮展示。</summary>
public List<HouseFaultRow> Faults { get; set; } = new List<HouseFaultRow>();
在文件内 HouseMonitorRow 类后(命名空间内)加:
/// <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; }
}
Run: dotnet test ...IvfTl.ControlHost.Tests.csproj --filter MonitorSnapshotFaultTests
Expected: PASS(2 绿)。
AppData.cs 在 MqttService 等属性附近(如 CurrentUser 属性后)加:
/// <summary>启动期坏舱清单(InitTL 写入,GetMonitorSnapshot 透出,ReportStartupFaults 上报)。</summary>
public List<HouseFault> StartupFaults { get; set; } = new List<HouseFault>();
确认文件顶部已 using IvfTl.Control.Entity.InitEntitys;(AppData 大量用 InitEntitys);若无则加。
AppData.cs:222 附近,构造 MonitorSnapshot 处)在 GetMonitorSnapshot 内、new MonitorSnapshot{...} 赋值各字段后(或返回前),把 StartupFaults 映射进快照:
// 舱故障透出(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)。
参照现有 MqttAlarm(AppData.cs:1350,走 HttpService.AlarmApi1)与 AlarmApi(HttpService.cs:54,POST /api/tl/control/alarm/reportCloudAlarm,带 houseSn/wellSn)。新增:
/// <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"); }
}
}
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: 提交
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绿)"
前置: 当前无活体培养(拔插不伤生物);UAC 可静默提权起停 control(见记忆 [[control-realmachine-smoke-startup]]);control 须从 operate 输出根的 control\ 子目录跑(..\tl-shared.config 才解析得到,见交接卡 2026-06-23 真机段第 4 点)。
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。预期:
started:true;snapshot.Faults(FaultType=HOUSE_CAMERA_FAULT/CcdSnMissing、Isolated:true),且不在可跑舱清单;[ ] Step 4: 告警落库核实 —— 用 JDBC(临时文件/DbSql.java 模式)连 108 查 aivfo-tl-control 告警表,确认该坏舱告警入库(alarmTypeKey + houseSn + 原因)。若被拒,记录:需 Java 告警字典登记新 alarmTypeKey(见「跨端依赖」),作为 Phase 4 门控项。
[ ] Step 5: 恢复 + 复测 —— 插回相机重启 control,确认该舱回到可跑、Faults 不再含它。
[ ] Step 6: 回写文档(见回写协议)+ 提交。
git add 项目文档/进度/进度状态.yaml 项目文档/进度/交接卡.md 项目文档/进度/工作计划表.md 项目文档/进度/待验证清单.md 项目文档/进度/进度数据.js
git commit -m "docs(house-fault): 第一阶段 control 坏舱剔除真机拔插验收回写"
HouseBin/BufferBottleBin.MainThread、ComBin.StartSendCommandThread 的 catch 里:同舱连续失败计数达阈值(去抖,避免每圈刷屏)→ 升级一条 HouseFault{RuntimeFault} 进 AppData.StartupFaults(改名/或新增 RuntimeFaults)+ ReportStartupFaults 同款上报 HOUSE_RUNTIME_FAULT;失败计数清零条件 = 该舱恢复一次成功通讯。ivf_tl_Operate 监控页(读 ControlClient → /status 的 snapshot.Faults)新增"舱故障"区:红色高亮列出每故障舱(舱号/类型/时间/已隔离),与现有监控三块并列。HOUSE_INIT_EXCLUDED/HOUSE_CAMERA_FAULT/HOUSE_SERIAL_FAULT/HOUSE_RUNTIME_FAULT)需在 Java 告警字典/告警类型配置登记,否则 reportCloudAlarm 落库被拒、front 也无从展示。Task 7 Step 4 已先探;此阶段落地登记。待验证清单.md。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 调用,保证中途可编译)。✓