Files
XplorePlane/XplorePlane/Services/Cnc/CncProgramService.cs
T

456 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Services;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
namespace XplorePlane.Services.Cnc
{
/// <summary>
/// CNC 程序管理服务实现。
/// 负责程序创建、节点编辑(插入/删除/移动 + 自动重编号)、JSON 序列化/反序列化和 .xp 文件读写。
/// CNC program management service implementation.
/// Handles program creation, node editing (insert/remove/move + auto-renumber), JSON serialization and .xp file I/O.
/// </summary>
public class CncProgramService : ICncProgramService
{
private readonly IAppStateService _appStateService;
private readonly IRaySourceService _raySourceService;
private readonly ILoggerService _logger;
// ── 序列化配置 | Serialization options ──
private static readonly JsonSerializerOptions CncJsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
public CncProgramService(
IAppStateService appStateService,
IRaySourceService raySourceService,
ILoggerService logger)
{
ArgumentNullException.ThrowIfNull(appStateService);
ArgumentNullException.ThrowIfNull(raySourceService);
ArgumentNullException.ThrowIfNull(logger);
_appStateService = appStateService;
_raySourceService = raySourceService;
_logger = logger.ForModule<CncProgramService>();
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
}
/// <inheritdoc />
public CncProgram CreateProgram(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("程序名称不能为空 | Program name cannot be empty", nameof(name));
var now = DateTime.UtcNow;
var program = new CncProgram(
Id: Guid.NewGuid(),
Name: name,
CreatedAt: now,
UpdatedAt: now,
Nodes: Array.Empty<CncNode>());
_logger.Info("已创建 CNC 程序 | Created CNC program: {ProgramName}, Id={ProgramId}", name, program.Id);
return program;
}
/// <inheritdoc />
public CncNode CreateNode(CncNodeType type)
{
var id = Guid.NewGuid();
const int defaultIndex = 0;
CncNode node = type switch
{
// 参考点:捕获 6 轴位置 | Reference point: capture 6-axis position
CncNodeType.ReferencePoint => CreateReferencePointNode(id, defaultIndex),
// 保存节点(含图像):捕获全部设备状态 | Save node with image: capture all device states
CncNodeType.SaveNodeWithImage => CreateSaveNodeWithImageNode(id, defaultIndex),
// 保存节点(不含图像):捕获全部设备状态 | Save node: capture all device states
CncNodeType.SaveNode => CreateSaveNodeNode(id, defaultIndex),
// 保存位置:仅捕获运动状态 | Save position: capture motion state only
CncNodeType.SavePosition => CreateSavePositionNode(id, defaultIndex),
// 检测模块:空 Pipeline | Inspection module: empty pipeline
CncNodeType.InspectionModule => new InspectionModuleNode(
id, defaultIndex, $"检测模块_{defaultIndex}",
Pipeline: new PipelineModel()),
// 检测标记:默认标记值 | Inspection marker: default marker values
CncNodeType.InspectionMarker => new InspectionMarkerNode(
id, defaultIndex, $"检测标记_{defaultIndex}",
MarkerType: "Default",
MarkerX: 0.0, MarkerY: 0.0),
// 停顿对话框:默认标题/消息 | Pause dialog: default title/message
CncNodeType.PauseDialog => new PauseDialogNode(
id, defaultIndex, $"停顿_{defaultIndex}",
DialogTitle: "暂停",
DialogMessage: "请确认后继续"),
// 等待延时:默认 1000ms | Wait delay: default 1000ms
CncNodeType.WaitDelay => new WaitDelayNode(
id, defaultIndex, $"延时_{defaultIndex}",
DelayMilliseconds: 1000),
// 完成程序:默认名称 | Complete program: default name
CncNodeType.CompleteProgram => new CompleteProgramNode(
id, defaultIndex, "完成程序"),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, $"不支持的节点类型 | Unsupported node type: {type}")
};
_logger.Info("已创建节点 | Created node: Type={NodeType}, Id={NodeId}", type, id);
return node;
}
/// <inheritdoc />
public CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node)
{
ArgumentNullException.ThrowIfNull(program);
ArgumentNullException.ThrowIfNull(node);
// 防止重复插入 CompleteProgram | Prevent duplicate CompleteProgram insertion
if (node.NodeType == CncNodeType.CompleteProgram &&
program.Nodes.Any(n => n.NodeType == CncNodeType.CompleteProgram))
{
throw new InvalidOperationException(
"CNC 程序中已存在完成程序节点,不允许重复插入 | A CompleteProgram node already exists, duplicate insertion is not allowed");
}
var nodes = new List<CncNode>(program.Nodes);
// afterIndex = -1 表示插入到开头 | afterIndex = -1 means insert at the beginning
int insertAt = afterIndex + 1;
if (insertAt < 0) insertAt = 0;
if (insertAt > nodes.Count) insertAt = nodes.Count;
nodes.Insert(insertAt, node);
var updated = program with
{
Nodes = RenumberNodes(nodes),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("已插入节点 | Inserted node: Type={NodeType}, AfterIndex={AfterIndex}, Program={ProgramName}",
node.NodeType, afterIndex, program.Name);
return updated;
}
/// <inheritdoc />
public CncProgram RemoveNode(CncProgram program, int index)
{
ArgumentNullException.ThrowIfNull(program);
if (index < 0 || index >= program.Nodes.Count)
throw new ArgumentOutOfRangeException(nameof(index),
$"索引超出范围 | Index out of range: {index}, Count={program.Nodes.Count}");
var nodes = new List<CncNode>(program.Nodes);
nodes.RemoveAt(index);
var updated = program with
{
Nodes = RenumberNodes(nodes),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("已移除节点 | Removed node: Index={Index}, Program={ProgramName}",
index, program.Name);
return updated;
}
/// <inheritdoc />
public CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex)
{
ArgumentNullException.ThrowIfNull(program);
if (oldIndex < 0 || oldIndex >= program.Nodes.Count)
throw new ArgumentOutOfRangeException(nameof(oldIndex),
$"旧索引超出范围 | Old index out of range: {oldIndex}, Count={program.Nodes.Count}");
if (newIndex < 0 || newIndex >= program.Nodes.Count)
throw new ArgumentOutOfRangeException(nameof(newIndex),
$"新索引超出范围 | New index out of range: {newIndex}, Count={program.Nodes.Count}");
var nodes = new List<CncNode>(program.Nodes);
var node = nodes[oldIndex];
nodes.RemoveAt(oldIndex);
nodes.Insert(newIndex, node);
var updated = program with
{
Nodes = RenumberNodes(nodes),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("已移动节点 | Moved node: OldIndex={OldIndex} -> NewIndex={NewIndex}, Program={ProgramName}",
oldIndex, newIndex, program.Name);
return updated;
}
/// <inheritdoc />
public CncProgram UpdateNode(CncProgram program, int index, CncNode node)
{
ArgumentNullException.ThrowIfNull(program);
ArgumentNullException.ThrowIfNull(node);
if (index < 0 || index >= program.Nodes.Count)
throw new ArgumentOutOfRangeException(nameof(index),
$"Index out of range: {index}, Count={program.Nodes.Count}");
var nodes = new List<CncNode>(program.Nodes)
{
[index] = node with { Index = index }
};
var updated = program with
{
Nodes = nodes.AsReadOnly(),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("Updated node: Index={Index}, Type={NodeType}, Program={ProgramName}",
index, node.NodeType, program.Name);
return updated;
}
/// <inheritdoc />
public async Task SaveAsync(CncProgram program, string filePath)
{
ArgumentNullException.ThrowIfNull(program);
ArgumentNullException.ThrowIfNull(filePath);
// 路径安全性验证:检查路径遍历 | Path safety: check for path traversal
if (filePath.Contains("..", StringComparison.Ordinal))
throw new UnauthorizedAccessException(
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
var json = Serialize(program);
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
_logger.Info("已保存 CNC 程序到文件 | Saved CNC program to file: {FilePath}, Program={ProgramName}",
filePath, program.Name);
}
/// <inheritdoc />
public async Task<CncProgram> LoadAsync(string filePath)
{
ArgumentNullException.ThrowIfNull(filePath);
// 路径安全性验证 | Path safety check
if (filePath.Contains("..", StringComparison.Ordinal))
throw new UnauthorizedAccessException(
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
if (!File.Exists(filePath))
throw new FileNotFoundException(
$"CNC 程序文件不存在 | CNC program file not found: {filePath}", filePath);
string json;
try
{
json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not FileNotFoundException)
{
throw new InvalidDataException(
$"无法读取 CNC 程序文件 | Cannot read CNC program file: {filePath}", ex);
}
var program = Deserialize(json);
_logger.Info("已加载 CNC 程序 | Loaded CNC program: {FilePath}, Program={ProgramName}, Nodes={NodeCount}",
filePath, program.Name, program.Nodes.Count);
return program;
}
/// <inheritdoc />
public string Serialize(CncProgram program)
{
ArgumentNullException.ThrowIfNull(program);
return JsonSerializer.Serialize(program, CncJsonOptions);
}
/// <inheritdoc />
public CncProgram Deserialize(string json)
{
if (string.IsNullOrWhiteSpace(json))
throw new InvalidDataException("JSON 字符串为空 | JSON string is empty");
CncProgram program;
try
{
program = JsonSerializer.Deserialize<CncProgram>(json, CncJsonOptions);
}
catch (JsonException ex)
{
throw new InvalidDataException(
$"CNC 程序 JSON 格式无效 | Invalid CNC program JSON format: {ex.Message}", ex);
}
if (program is null)
throw new InvalidDataException("反序列化结果为 null | Deserialization result is null");
if (string.IsNullOrWhiteSpace(program.Name))
throw new InvalidDataException("CNC 程序数据不完整,缺少名称 | CNC program data incomplete, missing Name");
if (program.Nodes is null)
throw new InvalidDataException("CNC 程序数据不完整,缺少节点列表 | CNC program data incomplete, missing Nodes");
// 对节点应用值域校验(如 WaitDelay 截断)| Apply value range validation to nodes
var validatedNodes = ValidateNodes(program.Nodes);
return program with { Nodes = validatedNodes };
}
// ── 常量 | Constants ──
/// <summary>等待延时最小值(毫秒)| Wait delay minimum (ms)</summary>
private const int WaitDelayMin = 0;
/// <summary>等待延时最大值(毫秒)| Wait delay maximum (ms)</summary>
private const int WaitDelayMax = 300000;
// ── 内部辅助方法 | Internal helper methods ──
/// <summary>
/// 将等待延时值截断到 [0, 300000] 范围 | Clamp wait delay value to [0, 300000] range
/// </summary>
internal static int ClampWaitDelay(int value) =>
Math.Clamp(value, WaitDelayMin, WaitDelayMax);
/// <summary>
/// 对反序列化后的节点列表应用值域校验(如 WaitDelay 截断)
/// Apply value range validation to deserialized nodes (e.g. WaitDelay clamping)
/// </summary>
private static IReadOnlyList<CncNode> ValidateNodes(IReadOnlyList<CncNode> nodes)
{
var result = new List<CncNode>(nodes.Count);
foreach (var node in nodes)
{
if (node is WaitDelayNode wdn)
{
var clamped = ClampWaitDelay(wdn.DelayMilliseconds);
result.Add(clamped != wdn.DelayMilliseconds ? wdn with { DelayMilliseconds = clamped } : wdn);
}
else
{
result.Add(node);
}
}
return result.AsReadOnly();
}
/// <summary>
/// 重新编号节点索引为连续序列 0, 1, 2, ..., N-1record 不可变,使用 with 语法)
/// Renumber node indices to consecutive sequence 0, 1, 2, ..., N-1 (records are immutable, use with syntax)
/// </summary>
private static IReadOnlyList<CncNode> RenumberNodes(List<CncNode> nodes)
{
var result = new List<CncNode>(nodes.Count);
int referencePointNumber = 0;
int savePositionNumber = 0;
int inspectionModuleNumber = 0;
for (int i = 0; i < nodes.Count; i++)
{
var indexedNode = nodes[i] with { Index = i };
result.Add(indexedNode switch
{
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4FDD\u5B58\u4F4D\u7F6E_{savePositionNumber++}" },
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u68C0\u6D4B\u6A21\u5757_{inspectionModuleNumber++}" },
_ => indexedNode
});
}
return result.AsReadOnly();
}
/// <summary>创建参考点节点 | Create reference point node</summary>
private ReferencePointNode CreateReferencePointNode(Guid id, int index)
{
var motion = _appStateService.MotionState;
var raySource = _appStateService.RaySourceState;
return new ReferencePointNode(
id, index, $"参考点_{index}",
StageX: motion.StageX,
StageY: motion.StageY,
SourceZ: motion.SourceZ,
DetectorZ: motion.DetectorZ,
DetectorSwing: motion.DetectorSwing,
FDD: motion.FDD,
IsRayOn: raySource.IsOn,
Voltage: raySource.Voltage,
Current: TryReadCurrent(),
StageRotation: motion.StageRotation,
FixtureRotation: motion.FixtureRotation,
FOD: motion.FOD,
Magnification: motion.Magnification);
}
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
private SaveNodeWithImageNode CreateSaveNodeWithImageNode(Guid id, int index)
{
return new SaveNodeWithImageNode(
id, index, $"保存节点_图像_{index}",
MotionState: _appStateService.MotionState,
RaySourceState: _appStateService.RaySourceState,
DetectorState: _appStateService.DetectorState,
ImageFileName: "");
}
/// <summary>创建保存节点(不含图像)| Create save node without image</summary>
private SaveNodeNode CreateSaveNodeNode(Guid id, int index)
{
return new SaveNodeNode(
id, index, $"保存节点_{index}",
MotionState: _appStateService.MotionState,
RaySourceState: _appStateService.RaySourceState,
DetectorState: _appStateService.DetectorState);
}
/// <summary>创建保存位置节点 | Create save position node</summary>
private SavePositionNode CreateSavePositionNode(Guid id, int index)
{
return new SavePositionNode(
id, index, $"检测位置_{index}",
MotionState: _appStateService.MotionState);
}
private double TryReadCurrent()
{
try
{
var result = _raySourceService.ReadCurrent();
if (result?.Success == true)
{
return result.GetFloat();
}
_logger.Warn("Failed to read ray source current, ReferencePoint node will use 0");
}
catch (Exception ex)
{
_logger.Warn("Failed to read ray source current: {Message}", ex.Message);
}
return 0d;
}
}
}