b95941cb50
在 CNC 编排层补齐 P0 硬件控制能力,使检测编排可覆盖设备测试与校准流程。 位置节点硬件状态还原(核心): - SavePositionNode 新增可选探测器采集参数快照,兼容旧 .xp 文件 - 执行到位后按序还原射线源电压/电流与探测器参数(先电压后电流) - 射线未开弹窗提醒并暂停;还原失败弹窗提示并暂停不继续;服务不可用降级跳过 - 新增 ICncInteractionService 封装弹窗交互,便于单元测试 四类硬件动作节点: - 射线源控制:开/关/设参/暖机/训机,安全时序 + 稳定等待 - 探测器校正:暗场/增益/坏像素/自动 - 安全门控制:开门/关门,复用内置联锁 - 几何设置:按 FDD 或放大倍率反算定位 模型、编辑器与工具栏接线: - CncNodeType 追加四枚举值 + 四类节点 record + 多态判别符 + 值域校验 - 编辑器新增插入命令、类型判定、图标与工具栏入口 - IDetectorService 预留当前采集参数读取接口(暂返回 null,待硬件层完善) 系统设置: - 新增数据存储根目录与日志目录配置及浏览按钮 - 修复保存时重复打开配置文件导致的配置冲突异常 测试: - 新增 33 个单元测试,覆盖序列化往返、旧文件兼容、节点分派/降级/失败、 位置还原全路径与混合程序执行
216 lines
9.6 KiB
C#
216 lines
9.6 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Moq;
|
||
using XP.Common.Logging.Interfaces;
|
||
using XP.Hardware.RaySource.Services;
|
||
using XplorePlane.Models;
|
||
using XplorePlane.Services.AppState;
|
||
using XplorePlane.Services.Cnc;
|
||
using XplorePlane.Services.Permission;
|
||
using Xunit;
|
||
|
||
namespace XplorePlane.Tests.Services
|
||
{
|
||
public class CncProgramServiceTests
|
||
{
|
||
[Fact]
|
||
public void SerializeDeserialize_SavePosition_PreservesSaveImage()
|
||
{
|
||
var appState = new Mock<IAppStateService>();
|
||
appState.SetupGet(s => s.MotionState).Returns(MotionState.Default);
|
||
appState.SetupGet(s => s.RaySourceState).Returns(RaySourceState.Default);
|
||
appState.SetupGet(s => s.DetectorState).Returns(DetectorState.Default);
|
||
|
||
var raySource = new Mock<IRaySourceService>();
|
||
var logger = new Mock<ILoggerService>();
|
||
logger.Setup(l => l.ForModule<CncProgramService>()).Returns(logger.Object);
|
||
|
||
var permissionService = new Mock<IPermissionService>();
|
||
permissionService.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
|
||
|
||
var service = new CncProgramService(appState.Object, raySource.Object, logger.Object, permissionService.Object);
|
||
var program = new CncProgram(
|
||
Guid.NewGuid(),
|
||
"Program",
|
||
DateTime.UtcNow,
|
||
DateTime.UtcNow,
|
||
new List<CncNode>
|
||
{
|
||
new SavePositionNode(Guid.NewGuid(), 0, "保存位置_0", MotionState.Default, SaveImage: true)
|
||
}.AsReadOnly());
|
||
|
||
var json = service.Serialize(program);
|
||
var deserialized = service.Deserialize(json);
|
||
|
||
var savePosition = Assert.IsType<SavePositionNode>(Assert.Single(deserialized.Nodes));
|
||
Assert.True(savePosition.SaveImage);
|
||
}
|
||
|
||
private static CncProgramService CreateService()
|
||
{
|
||
var appState = new Mock<IAppStateService>();
|
||
appState.SetupGet(s => s.MotionState).Returns(MotionState.Default);
|
||
appState.SetupGet(s => s.RaySourceState).Returns(RaySourceState.Default);
|
||
appState.SetupGet(s => s.DetectorState).Returns(DetectorState.Default);
|
||
|
||
var raySource = new Mock<IRaySourceService>();
|
||
var logger = new Mock<ILoggerService>();
|
||
logger.Setup(l => l.ForModule<CncProgramService>()).Returns(logger.Object);
|
||
|
||
var permissionService = new Mock<IPermissionService>();
|
||
permissionService.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
|
||
|
||
return new CncProgramService(appState.Object, raySource.Object, logger.Object, permissionService.Object);
|
||
}
|
||
|
||
private static CncProgram Roundtrip(CncProgramService service, params CncNode[] nodes)
|
||
{
|
||
var program = new CncProgram(
|
||
Guid.NewGuid(), "Program", DateTime.UtcNow, DateTime.UtcNow,
|
||
new List<CncNode>(nodes).AsReadOnly());
|
||
var json = service.Serialize(program);
|
||
return service.Deserialize(json);
|
||
}
|
||
|
||
[Fact]
|
||
public void SerializeDeserialize_RaySourceControlNode_PreservesAllFields()
|
||
{
|
||
var service = CreateService();
|
||
var node = new RaySourceControlNode(Guid.NewGuid(), 0, "射线源控制_0",
|
||
RaySourceAction.TurnOn, Voltage: 120, Current: 200, WaitForStable: true, StableTimeoutMs: 8000);
|
||
|
||
var result = Assert.IsType<RaySourceControlNode>(Assert.Single(Roundtrip(service, node).Nodes));
|
||
Assert.Equal(RaySourceAction.TurnOn, result.Action);
|
||
Assert.Equal(120, result.Voltage);
|
||
Assert.Equal(200, result.Current);
|
||
Assert.True(result.WaitForStable);
|
||
Assert.Equal(8000, result.StableTimeoutMs);
|
||
}
|
||
|
||
[Fact]
|
||
public void SerializeDeserialize_DetectorCorrectionNode_PreservesAllFields()
|
||
{
|
||
var service = CreateService();
|
||
var node = new DetectorCorrectionNode(Guid.NewGuid(), 0, "探测器校正_0",
|
||
DetectorCorrectionType.Dark, FrameCount: 20);
|
||
|
||
var result = Assert.IsType<DetectorCorrectionNode>(Assert.Single(Roundtrip(service, node).Nodes));
|
||
Assert.Equal(DetectorCorrectionType.Dark, result.CorrectionType);
|
||
Assert.Equal(20, result.FrameCount);
|
||
}
|
||
|
||
[Fact]
|
||
public void SerializeDeserialize_DoorControlNode_PreservesAllFields()
|
||
{
|
||
var service = CreateService();
|
||
var node = new DoorControlNode(Guid.NewGuid(), 0, "安全门_0",
|
||
DoorAction.Open, WaitForComplete: true, TimeoutMs: 12000);
|
||
|
||
var result = Assert.IsType<DoorControlNode>(Assert.Single(Roundtrip(service, node).Nodes));
|
||
Assert.Equal(DoorAction.Open, result.Action);
|
||
Assert.True(result.WaitForComplete);
|
||
Assert.Equal(12000, result.TimeoutMs);
|
||
}
|
||
|
||
[Fact]
|
||
public void SerializeDeserialize_GeometryNode_PreservesAllFields()
|
||
{
|
||
var service = CreateService();
|
||
var node = new GeometryNode(Guid.NewGuid(), 0, "几何_0",
|
||
GeometryMode.ByMagnification, Fod: 50.5, Fdd: 0, Magnification: 4.0, WaitForSettled: true);
|
||
|
||
var result = Assert.IsType<GeometryNode>(Assert.Single(Roundtrip(service, node).Nodes));
|
||
Assert.Equal(GeometryMode.ByMagnification, result.Mode);
|
||
Assert.Equal(50.5, result.Fod);
|
||
Assert.Equal(4.0, result.Magnification);
|
||
Assert.True(result.WaitForSettled);
|
||
}
|
||
|
||
[Fact]
|
||
public void SerializeDeserialize_SavePositionWithDetectorAcq_PreservesSnapshot()
|
||
{
|
||
var service = CreateService();
|
||
var node = new SavePositionNode(Guid.NewGuid(), 0, "检测位置_0", MotionState.Default,
|
||
RaySourceState: new RaySourceState(true, 100, 150),
|
||
SaveImage: true,
|
||
DetectorAcq: new DetectorAcquisitionSnapshot(BinningIndex: 1, Pga: 3, FrameRate: 15m));
|
||
|
||
var result = Assert.IsType<SavePositionNode>(Assert.Single(Roundtrip(service, node).Nodes));
|
||
Assert.NotNull(result.DetectorAcq);
|
||
Assert.Equal(1, result.DetectorAcq.BinningIndex);
|
||
Assert.Equal(3, result.DetectorAcq.Pga);
|
||
Assert.Equal(15m, result.DetectorAcq.FrameRate);
|
||
Assert.Equal(100, result.RaySourceState.Voltage);
|
||
Assert.Equal(150, result.RaySourceState.Power);
|
||
}
|
||
|
||
[Fact]
|
||
public void Deserialize_LegacySavePositionWithoutDetectorAcq_LoadsWithNullSnapshot()
|
||
{
|
||
var service = CreateService();
|
||
// 模拟旧 .xp:SavePosition 节点不含 DetectorAcq 字段
|
||
var legacyJson = @"{
|
||
""Id"": ""11111111-1111-1111-1111-111111111111"",
|
||
""Name"": ""LegacyProgram"",
|
||
""CreatedAt"": ""2024-01-01T00:00:00Z"",
|
||
""UpdatedAt"": ""2024-01-01T00:00:00Z"",
|
||
""Nodes"": [
|
||
{
|
||
""$type"": ""SavePosition"",
|
||
""Id"": ""22222222-2222-2222-2222-222222222222"",
|
||
""Index"": 0,
|
||
""Name"": ""检测位置_0"",
|
||
""MotionState"": null,
|
||
""SaveImage"": true
|
||
}
|
||
]
|
||
}";
|
||
|
||
var deserialized = service.Deserialize(legacyJson);
|
||
var savePosition = Assert.IsType<SavePositionNode>(Assert.Single(deserialized.Nodes));
|
||
Assert.Null(savePosition.DetectorAcq);
|
||
Assert.True(savePosition.SaveImage);
|
||
}
|
||
|
||
[Fact]
|
||
public void Deserialize_DetectorCorrectionFrameCountOutOfRange_IsClamped()
|
||
{
|
||
var service = CreateService();
|
||
// FrameCount 超上限(9999)应被截断到 1000
|
||
var node = new DetectorCorrectionNode(Guid.NewGuid(), 0, "校正", DetectorCorrectionType.Dark, FrameCount: 9999);
|
||
var result = Assert.IsType<DetectorCorrectionNode>(Assert.Single(Roundtrip(service, node).Nodes));
|
||
Assert.Equal(1000, result.FrameCount);
|
||
}
|
||
|
||
[Fact]
|
||
public void Deserialize_RaySourceStableTimeoutOutOfRange_IsClamped()
|
||
{
|
||
var service = CreateService();
|
||
// StableTimeoutMs 超上限(999999)应被截断到 600000
|
||
var node = new RaySourceControlNode(Guid.NewGuid(), 0, "射线源", RaySourceAction.TurnOn, 120, 200, WaitForStable: true, StableTimeoutMs: 999999);
|
||
var result = Assert.IsType<RaySourceControlNode>(Assert.Single(Roundtrip(service, node).Nodes));
|
||
Assert.Equal(600000, result.StableTimeoutMs);
|
||
}
|
||
|
||
[Fact]
|
||
public void SerializeDeserialize_MixedHardwareProgram_PreservesAllNodes()
|
||
{
|
||
var service = CreateService();
|
||
var geo = new GeometryNode(Guid.NewGuid(), 0, "几何", GeometryMode.ByFdd, 50, 200);
|
||
var pos = new SavePositionNode(Guid.NewGuid(), 1, "位置", MotionState.Default,
|
||
RaySourceState: new RaySourceState(true, 100, 150),
|
||
DetectorAcq: new DetectorAcquisitionSnapshot(1, 2, 15m));
|
||
var corr = new DetectorCorrectionNode(Guid.NewGuid(), 2, "校正", DetectorCorrectionType.Auto, 10);
|
||
|
||
var rt = Roundtrip(service, geo, pos, corr);
|
||
|
||
Assert.Equal(3, rt.Nodes.Count);
|
||
Assert.IsType<GeometryNode>(rt.Nodes[0]);
|
||
var posRt = Assert.IsType<SavePositionNode>(rt.Nodes[1]);
|
||
Assert.IsType<DetectorCorrectionNode>(rt.Nodes[2]);
|
||
Assert.NotNull(posRt.DetectorAcq);
|
||
Assert.Equal(15m, posRt.DetectorAcq.FrameRate);
|
||
}
|
||
}
|
||
}
|