feat: 硬件虚拟化与CNC联动集成 - 运动控制/射线源模拟实现,CNC执行联动增强
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
|
||||
namespace XP.Hardware.MotionControl.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟轴复位实现 | Simulated Axis Reset Implementation
|
||||
/// 所有操作为空操作,始终报告复位已完成
|
||||
/// All operations are no-ops, always reports reset as done
|
||||
/// </summary>
|
||||
public class SimulatedAxisReset : IAxisReset
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool IsResetDone => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MotionResult Reset()
|
||||
{
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateStatus()
|
||||
{
|
||||
// 虚拟轴复位无需从 PLC 轮询状态 | No PLC polling needed for simulated axis reset
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
|
||||
namespace XP.Hardware.MotionControl.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟摇杆实现 | Simulated Joystick Implementation
|
||||
/// 所有操作为空操作,始终报告摇杆未激活
|
||||
/// All operations are no-ops, always reports joystick as inactive
|
||||
/// </summary>
|
||||
public class SimulatedJoystick : IJoystick
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool IsJoystickActive => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateStatus()
|
||||
{
|
||||
// 虚拟摇杆无需从 PLC 轮询状态 | No PLC polling needed for simulated joystick
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
|
||||
namespace XP.Hardware.MotionControl.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟直线轴实现 | Simulated Linear Axis Implementation
|
||||
/// 通过后台任务线性插值模拟轴移动,无需真实 PLC 硬件
|
||||
/// Simulates axis movement via background task with linear interpolation, no real PLC required
|
||||
/// </summary>
|
||||
public class SimulatedLinearAxis : LinearAxisBase
|
||||
{
|
||||
private const int UpdateIntervalMs = 20;
|
||||
|
||||
private readonly double _defaultSpeed;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private CancellationTokenSource _moveCts;
|
||||
private Task _moveTask;
|
||||
private double _jogSpeedPercent = 50.0;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="axisId">轴标识 | Axis identifier</param>
|
||||
/// <param name="min">最小位置(mm)| Minimum position (mm)</param>
|
||||
/// <param name="max">最大位置(mm)| Maximum position (mm)</param>
|
||||
/// <param name="origin">原点偏移(mm)| Origin offset (mm)</param>
|
||||
/// <param name="defaultSpeed">默认速度(mm/s)| Default speed (mm/s)</param>
|
||||
public SimulatedLinearAxis(AxisId axisId, double min, double max, double origin, double defaultSpeed = 50.0)
|
||||
: base(axisId, min, max, origin)
|
||||
{
|
||||
_defaultSpeed = defaultSpeed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult MoveToTarget(double target, double? speed = null)
|
||||
{
|
||||
if (!ValidateTarget(target))
|
||||
return MotionResult.Fail($"[Simulated] 目标位置 {target} 超出范围 [{_min}, {_max}]");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (Status == AxisStatus.Moving)
|
||||
{
|
||||
// 取消当前移动,启动新移动
|
||||
CancelCurrentMove();
|
||||
}
|
||||
|
||||
var moveSpeed = speed ?? _defaultSpeed;
|
||||
Status = AxisStatus.Moving;
|
||||
|
||||
_moveCts = new CancellationTokenSource();
|
||||
var token = _moveCts.Token;
|
||||
|
||||
_moveTask = Task.Run(() => ExecuteMove(target, moveSpeed, token), token);
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult Stop()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
Status = AxisStatus.Idle;
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult Home()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (Status == AxisStatus.Moving)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
}
|
||||
|
||||
Status = AxisStatus.Homing;
|
||||
|
||||
_moveCts = new CancellationTokenSource();
|
||||
var token = _moveCts.Token;
|
||||
|
||||
_moveTask = Task.Run(async () =>
|
||||
{
|
||||
await ExecuteMove(0, _defaultSpeed, token);
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
Status = AxisStatus.Idle;
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult JogStart(bool positive)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (Status == AxisStatus.Moving || Status == AxisStatus.Homing)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
}
|
||||
|
||||
var target = positive ? _max : _min;
|
||||
var jogSpeed = _defaultSpeed * (_jogSpeedPercent / 100.0);
|
||||
|
||||
Status = AxisStatus.Moving;
|
||||
|
||||
_moveCts = new CancellationTokenSource();
|
||||
var token = _moveCts.Token;
|
||||
|
||||
_moveTask = Task.Run(() => ExecuteMove(target, jogSpeed, token), token);
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult JogStop()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
Status = AxisStatus.Idle;
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult SetJogSpeed(double speedPercent)
|
||||
{
|
||||
if (speedPercent < 0 || speedPercent > 100)
|
||||
return MotionResult.Fail($"[Simulated] Jog 速度百分比 {speedPercent} 超出范围 [0, 100]");
|
||||
|
||||
_jogSpeedPercent = speedPercent;
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void UpdateStatus()
|
||||
{
|
||||
// 虚拟轴无需从 PLC 轮询状态 | No PLC polling needed for simulated axis
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行线性插值移动 | Execute linear interpolation move
|
||||
/// </summary>
|
||||
private async Task ExecuteMove(double target, double speed, CancellationToken token)
|
||||
{
|
||||
var startPosition = ActualPosition;
|
||||
var distance = target - startPosition;
|
||||
|
||||
if (Math.Abs(distance) < 0.001)
|
||||
{
|
||||
Status = AxisStatus.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
var totalTime = Math.Abs(distance) / speed; // seconds
|
||||
var elapsed = 0.0;
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(UpdateIntervalMs, token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
elapsed += UpdateIntervalMs / 1000.0;
|
||||
|
||||
if (elapsed >= totalTime)
|
||||
{
|
||||
ActualPosition = target;
|
||||
UpdateLimitFlags();
|
||||
Status = AxisStatus.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
var progress = elapsed / totalTime;
|
||||
ActualPosition = startPosition + distance * progress;
|
||||
UpdateLimitFlags();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消当前移动任务 | Cancel current move task
|
||||
/// </summary>
|
||||
private void CancelCurrentMove()
|
||||
{
|
||||
if (_moveCts != null)
|
||||
{
|
||||
_moveCts.Cancel();
|
||||
_moveCts.Dispose();
|
||||
_moveCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新限位标志 | Update limit flags
|
||||
/// </summary>
|
||||
private void UpdateLimitFlags()
|
||||
{
|
||||
PositiveLimitHit = ActualPosition >= _max;
|
||||
NegativeLimitHit = ActualPosition <= _min;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Config;
|
||||
|
||||
namespace XP.Hardware.MotionControl.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟运动系统实现 | Simulated Motion System Implementation
|
||||
/// 根据 MotionControlConfig 创建虚拟轴实例,无需真实 PLC 硬件
|
||||
/// Creates simulated axis instances from MotionControlConfig, no real PLC required
|
||||
/// </summary>
|
||||
public class SimulatedMotionSystem : IMotionSystem
|
||||
{
|
||||
private readonly Dictionary<AxisId, ILinearAxis> _linearAxes = new();
|
||||
private readonly Dictionary<RotaryAxisId, IRotaryAxis> _rotaryAxes = new();
|
||||
private readonly ISafetyDoor _safetyDoor;
|
||||
private readonly IJoystick _joystick;
|
||||
private readonly IAxisReset _axisReset;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="config">运动控制配置 | Motion control configuration</param>
|
||||
public SimulatedMotionSystem(MotionControlConfig config)
|
||||
{
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
// 创建直线轴 | Create linear axes
|
||||
if (config.LinearAxes.TryGetValue(AxisId.SourceZ, out var sz))
|
||||
_linearAxes[AxisId.SourceZ] = new SimulatedLinearAxis(AxisId.SourceZ, sz.Min, sz.Max, sz.Origin);
|
||||
|
||||
if (config.LinearAxes.TryGetValue(AxisId.DetectorZ, out var dz))
|
||||
_linearAxes[AxisId.DetectorZ] = new SimulatedLinearAxis(AxisId.DetectorZ, dz.Min, dz.Max, dz.Origin);
|
||||
|
||||
if (config.LinearAxes.TryGetValue(AxisId.StageX, out var sx))
|
||||
_linearAxes[AxisId.StageX] = new SimulatedLinearAxis(AxisId.StageX, sx.Min, sx.Max, sx.Origin);
|
||||
|
||||
if (config.LinearAxes.TryGetValue(AxisId.StageY, out var sy))
|
||||
_linearAxes[AxisId.StageY] = new SimulatedLinearAxis(AxisId.StageY, sy.Min, sy.Max, sy.Origin);
|
||||
|
||||
// 创建旋转轴 | Create rotary axes
|
||||
if (config.RotaryAxes.TryGetValue(RotaryAxisId.DetectorSwing, out var ds))
|
||||
_rotaryAxes[RotaryAxisId.DetectorSwing] = new SimulatedRotaryAxis(RotaryAxisId.DetectorSwing, ds.Min, ds.Max, ds.Enabled);
|
||||
|
||||
if (config.RotaryAxes.TryGetValue(RotaryAxisId.StageRotation, out var sr))
|
||||
_rotaryAxes[RotaryAxisId.StageRotation] = new SimulatedRotaryAxis(RotaryAxisId.StageRotation, sr.Min, sr.Max, sr.Enabled);
|
||||
|
||||
if (config.RotaryAxes.TryGetValue(RotaryAxisId.FixtureRotation, out var fr))
|
||||
_rotaryAxes[RotaryAxisId.FixtureRotation] = new SimulatedRotaryAxis(RotaryAxisId.FixtureRotation, fr.Min, fr.Max, fr.Enabled);
|
||||
|
||||
// 创建辅助设备 | Create helper devices
|
||||
_safetyDoor = new SimulatedSafetyDoor();
|
||||
_joystick = new SimulatedJoystick();
|
||||
_axisReset = new SimulatedAxisReset();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISafetyDoor SafetyDoor => _safetyDoor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IJoystick Joystick => _joystick;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IAxisReset AxisReset => _axisReset;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<AxisId, ILinearAxis> LinearAxes => _linearAxes;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<RotaryAxisId, IRotaryAxis> RotaryAxes => _rotaryAxes;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILinearAxis GetLinearAxis(AxisId axisId)
|
||||
{
|
||||
if (_linearAxes.TryGetValue(axisId, out var axis)) return axis;
|
||||
throw new KeyNotFoundException($"[Simulated] 未找到直线轴 {axisId} | Linear axis {axisId} not found");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IRotaryAxis GetRotaryAxis(RotaryAxisId axisId)
|
||||
{
|
||||
if (_rotaryAxes.TryGetValue(axisId, out var axis)) return axis;
|
||||
throw new KeyNotFoundException($"[Simulated] 未找到旋转轴 {axisId} | Rotary axis {axisId} not found");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateAllStatus()
|
||||
{
|
||||
// 虚拟运动系统无需从 PLC 轮询状态 | No PLC polling needed for simulated motion system
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
|
||||
namespace XP.Hardware.MotionControl.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟旋转轴实现 | Simulated Rotary Axis Implementation
|
||||
/// 通过后台任务线性插值模拟旋转轴移动,无需真实 PLC 硬件
|
||||
/// Simulates rotary axis movement via background task with linear interpolation, no real PLC required
|
||||
/// </summary>
|
||||
public class SimulatedRotaryAxis : RotaryAxisBase
|
||||
{
|
||||
private const int UpdateIntervalMs = 20;
|
||||
|
||||
private readonly double _defaultSpeed;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private CancellationTokenSource _moveCts;
|
||||
private Task _moveTask;
|
||||
private double _jogSpeedPercent = 50.0;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="axisId">轴标识 | Axis identifier</param>
|
||||
/// <param name="minAngle">最小角度(度)| Minimum angle (degrees)</param>
|
||||
/// <param name="maxAngle">最大角度(度)| Maximum angle (degrees)</param>
|
||||
/// <param name="enabled">是否启用 | Is enabled</param>
|
||||
/// <param name="defaultSpeed">默认角速度(度/秒)| Default angular speed (degrees/s)</param>
|
||||
public SimulatedRotaryAxis(RotaryAxisId axisId, double minAngle, double maxAngle, bool enabled, double defaultSpeed = 30.0)
|
||||
: base(axisId, minAngle, maxAngle, enabled)
|
||||
{
|
||||
_defaultSpeed = defaultSpeed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult MoveToTarget(double targetAngle, double? speed = null)
|
||||
{
|
||||
if (!ValidateTarget(targetAngle))
|
||||
return MotionResult.Fail($"[Simulated] 目标角度 {targetAngle} 超出范围 [{_minAngle}, {_maxAngle}]");
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (Status == AxisStatus.Moving)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
}
|
||||
|
||||
var moveSpeed = speed ?? _defaultSpeed;
|
||||
Status = AxisStatus.Moving;
|
||||
|
||||
_moveCts = new CancellationTokenSource();
|
||||
var token = _moveCts.Token;
|
||||
|
||||
_moveTask = Task.Run(() => ExecuteMove(targetAngle, moveSpeed, token), token);
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult Stop()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
Status = AxisStatus.Idle;
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult Home()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (Status == AxisStatus.Moving)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
}
|
||||
|
||||
Status = AxisStatus.Homing;
|
||||
|
||||
_moveCts = new CancellationTokenSource();
|
||||
var token = _moveCts.Token;
|
||||
|
||||
_moveTask = Task.Run(async () =>
|
||||
{
|
||||
await ExecuteMove(0, _defaultSpeed, token);
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
Status = AxisStatus.Idle;
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult JogStart(bool positive)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (Status == AxisStatus.Moving || Status == AxisStatus.Homing)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
}
|
||||
|
||||
var target = positive ? _maxAngle : _minAngle;
|
||||
var jogSpeed = _defaultSpeed * (_jogSpeedPercent / 100.0);
|
||||
|
||||
Status = AxisStatus.Moving;
|
||||
|
||||
_moveCts = new CancellationTokenSource();
|
||||
var token = _moveCts.Token;
|
||||
|
||||
_moveTask = Task.Run(() => ExecuteMove(target, jogSpeed, token), token);
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult JogStop()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
CancelCurrentMove();
|
||||
Status = AxisStatus.Idle;
|
||||
}
|
||||
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override MotionResult SetJogSpeed(double speedPercent)
|
||||
{
|
||||
if (speedPercent < 0 || speedPercent > 100)
|
||||
return MotionResult.Fail($"[Simulated] Jog 速度百分比 {speedPercent} 超出范围 [0, 100]");
|
||||
|
||||
_jogSpeedPercent = speedPercent;
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void UpdateStatus()
|
||||
{
|
||||
// 虚拟轴无需从 PLC 轮询状态 | No PLC polling needed for simulated axis
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行线性插值旋转移动 | Execute linear interpolation rotary move
|
||||
/// </summary>
|
||||
private async Task ExecuteMove(double targetAngle, double speed, CancellationToken token)
|
||||
{
|
||||
var startAngle = ActualAngle;
|
||||
var distance = targetAngle - startAngle;
|
||||
|
||||
if (Math.Abs(distance) < 0.001)
|
||||
{
|
||||
Status = AxisStatus.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
var totalTime = Math.Abs(distance) / speed; // seconds
|
||||
var elapsed = 0.0;
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(UpdateIntervalMs, token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
elapsed += UpdateIntervalMs / 1000.0;
|
||||
|
||||
if (elapsed >= totalTime)
|
||||
{
|
||||
ActualAngle = targetAngle;
|
||||
Status = AxisStatus.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
var progress = elapsed / totalTime;
|
||||
ActualAngle = startAngle + distance * progress;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消当前移动任务 | Cancel current move task
|
||||
/// </summary>
|
||||
private void CancelCurrentMove()
|
||||
{
|
||||
if (_moveCts != null)
|
||||
{
|
||||
_moveCts.Cancel();
|
||||
_moveCts.Dispose();
|
||||
_moveCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
|
||||
namespace XP.Hardware.MotionControl.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟安全门实现 | Simulated Safety Door Implementation
|
||||
/// 始终报告门已关闭(安全状态),所有操作返回成功
|
||||
/// Always reports door as closed (safe state), all operations return success
|
||||
/// </summary>
|
||||
public class SimulatedSafetyDoor : ISafetyDoor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public DoorStatus Status { get; private set; } = DoorStatus.Closed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInterlocked => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MotionResult Open()
|
||||
{
|
||||
Status = DoorStatus.Open;
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MotionResult Close()
|
||||
{
|
||||
Status = DoorStatus.Closed;
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MotionResult Stop()
|
||||
{
|
||||
return MotionResult.Ok();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateStatus()
|
||||
{
|
||||
// 虚拟安全门无需从 PLC 轮询状态 | No PLC polling needed for simulated safety door
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using Prism.Ioc;
|
||||
using Prism.Modularity;
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.Resources;
|
||||
using XP.Common.Localization;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
@@ -31,8 +33,18 @@ namespace XP.Hardware.MotionControl.Module
|
||||
// 注册几何计算器(单例)| Register geometry calculator (singleton)
|
||||
containerRegistry.RegisterSingleton<GeometryCalculator>();
|
||||
|
||||
// 注册运动系统(单例)| Register motion system (singleton)
|
||||
containerRegistry.RegisterSingleton<IMotionSystem, PlcMotionSystem>();
|
||||
// 根据配置选择运动系统实现 | Select motion system implementation based on config
|
||||
var motionType = ConfigurationManager.AppSettings["MotionControl:Type"] ?? "PLC";
|
||||
if (motionType.Equals("Simulated", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
containerRegistry.RegisterSingleton<IMotionSystem, SimulatedMotionSystem>();
|
||||
System.Console.WriteLine("[MotionControlModule] [Simulated] 使用虚拟运动系统 | Using simulated motion system");
|
||||
}
|
||||
else
|
||||
{
|
||||
containerRegistry.RegisterSingleton<IMotionSystem, PlcMotionSystem>();
|
||||
System.Console.WriteLine("[MotionControlModule] 使用PLC运动系统 | Using PLC motion system");
|
||||
}
|
||||
|
||||
// 注册运动控制业务服务(单例)| Register motion control service (singleton)
|
||||
containerRegistry.RegisterSingleton<IMotionControlService, MotionControlService>();
|
||||
@@ -57,9 +69,18 @@ namespace XP.Hardware.MotionControl.Module
|
||||
// Initialize LocalizationHelper to use ILocalizationService for string lookup (supports Fallback Chain)
|
||||
LocalizationHelper.Initialize(localizationService);
|
||||
|
||||
// 启动 PLC 状态轮询 | Start PLC status polling
|
||||
var motionService = containerProvider.Resolve<IMotionControlService>();
|
||||
motionService.StartPolling();
|
||||
// 仅在非虚拟模式下启动 PLC 状态轮询 | Only start PLC polling when not in simulated mode
|
||||
var motionSystem = containerProvider.Resolve<IMotionSystem>();
|
||||
if (motionSystem is not SimulatedMotionSystem)
|
||||
{
|
||||
var motionService = containerProvider.Resolve<IMotionControlService>();
|
||||
motionService.StartPolling();
|
||||
System.Console.WriteLine("[MotionControlModule] PLC 轮询已启动 | PLC polling started");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Console.WriteLine("[MotionControlModule] [Simulated] 跳过 PLC 轮询 | Skipping PLC polling (simulated mode)");
|
||||
}
|
||||
|
||||
System.Console.WriteLine("[MotionControlModule] 模块已初始化 | Module initialized");
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace XP.Hardware.RaySource.Factories
|
||||
var instance = sourceType.ToUpperInvariant() switch
|
||||
{
|
||||
"COMET225" => CreateComet225RaySource(),
|
||||
"SIMULATED" => new SimulatedXRaySource(_eventAggregator, _logger),
|
||||
_ => throw new NotSupportedException($"不支持的射线源类型: {sourceType}")
|
||||
};
|
||||
|
||||
@@ -90,7 +91,8 @@ namespace XP.Hardware.RaySource.Factories
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
"Comet225"
|
||||
"Comet225",
|
||||
"Simulated"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using Prism.Events;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.RaySource.Abstractions;
|
||||
using XP.Hardware.RaySource.Abstractions.Enums;
|
||||
using XP.Hardware.RaySource.Abstractions.Events;
|
||||
|
||||
namespace XP.Hardware.RaySource.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 模拟射线源实现 | Simulated X-Ray Source Implementation
|
||||
/// 用于开发和调试环境,无需真实硬件 | For development and debugging without real hardware
|
||||
/// </summary>
|
||||
public class SimulatedXRaySource : XRaySourceBase
|
||||
{
|
||||
#region 依赖注入字段
|
||||
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部状态
|
||||
|
||||
private bool _isOn;
|
||||
private float _setVoltage;
|
||||
private float _setCurrent;
|
||||
private float _setFocus;
|
||||
private readonly Random _random = new Random();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,注入依赖
|
||||
/// </summary>
|
||||
/// <param name="eventAggregator">Prism 事件聚合器</param>
|
||||
/// <param name="logger">日志服务</param>
|
||||
public SimulatedXRaySource(IEventAggregator eventAggregator, ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_logger = logger?.ForModule<SimulatedXRaySource>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public override string SourceName => "Simulated X-Ray Source";
|
||||
|
||||
#endregion
|
||||
|
||||
#region IXRaySource 方法实现
|
||||
|
||||
/// <summary>
|
||||
/// 初始化射线源
|
||||
/// </summary>
|
||||
public override XRayResult Initialize()
|
||||
{
|
||||
_isInitialized = true;
|
||||
_logger.Info("[Simulated] 射线源初始化成功");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接 PVI 变量
|
||||
/// </summary>
|
||||
public override XRayResult ConnectVariables()
|
||||
{
|
||||
_isConnected = true;
|
||||
_logger.Info("[Simulated] PVI 变量连接成功");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开启射线
|
||||
/// </summary>
|
||||
public override XRayResult TurnOn()
|
||||
{
|
||||
_isOn = true;
|
||||
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(RaySourceStatus.Opened);
|
||||
_logger.Info("[Simulated] 射线源已开启");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭射线
|
||||
/// </summary>
|
||||
public override XRayResult TurnOff()
|
||||
{
|
||||
_isOn = false;
|
||||
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(RaySourceStatus.Closed);
|
||||
_logger.Info("[Simulated] 射线源已关闭");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置电压(kV)
|
||||
/// </summary>
|
||||
public override XRayResult SetVoltage(float voltage)
|
||||
{
|
||||
_setVoltage = voltage;
|
||||
_logger.Info("[Simulated] 设置电压: {Voltage} kV", voltage);
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置电流(μA)
|
||||
/// </summary>
|
||||
public override XRayResult SetCurrent(float current)
|
||||
{
|
||||
_setCurrent = current;
|
||||
_logger.Info("[Simulated] 设置电流: {Current} μA", current);
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置焦点
|
||||
/// </summary>
|
||||
public override XRayResult SetFocus(float focus)
|
||||
{
|
||||
_setFocus = focus;
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取实际电压值(模拟 ±2% 波动)
|
||||
/// </summary>
|
||||
public override XRayResult ReadVoltage()
|
||||
{
|
||||
float noise = (float)(_random.NextDouble() * 0.04 - 0.02); // -0.02 to +0.02
|
||||
float simulatedVoltage = _setVoltage * (1 + noise);
|
||||
return XRayResult.Ok(simulatedVoltage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取实际电流值(模拟 ±2% 波动)
|
||||
/// </summary>
|
||||
public override XRayResult ReadCurrent()
|
||||
{
|
||||
float noise = (float)(_random.NextDouble() * 0.04 - 0.02); // -0.02 to +0.02
|
||||
float simulatedCurrent = _setCurrent * (1 + noise);
|
||||
return XRayResult.Ok(simulatedCurrent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取系统状态
|
||||
/// </summary>
|
||||
public override XRayResult ReadSystemStatus()
|
||||
{
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查错误状态
|
||||
/// </summary>
|
||||
public override XRayResult CheckErrors()
|
||||
{
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TXI 开启
|
||||
/// </summary>
|
||||
public override XRayResult TxiOn()
|
||||
{
|
||||
_logger.Info("[Simulated] TXI 已开启");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TXI 关闭
|
||||
/// </summary>
|
||||
public override XRayResult TxiOff()
|
||||
{
|
||||
_logger.Info("[Simulated] TXI 已关闭");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暖机设置
|
||||
/// </summary>
|
||||
public override XRayResult WarmUp()
|
||||
{
|
||||
_logger.Info("[Simulated] 暖机完成");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 训机设置
|
||||
/// </summary>
|
||||
public override XRayResult Training()
|
||||
{
|
||||
_logger.Info("[Simulated] 训机完成");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 灯丝校准
|
||||
/// </summary>
|
||||
public override XRayResult FilamentCalibration()
|
||||
{
|
||||
_logger.Info("[Simulated] 灯丝校准完成");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全部电压自动定心
|
||||
/// </summary>
|
||||
public override XRayResult AutoCenter()
|
||||
{
|
||||
_logger.Info("[Simulated] 自动定心完成");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置功率模式
|
||||
/// </summary>
|
||||
public override XRayResult SetPowerMode(int mode)
|
||||
{
|
||||
_logger.Info("[Simulated] 设置功率模式: {Mode}", mode);
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完全关闭设备,重置所有状态
|
||||
/// </summary>
|
||||
public override XRayResult CloseOff()
|
||||
{
|
||||
_isOn = false;
|
||||
_isConnected = false;
|
||||
_isInitialized = false;
|
||||
_logger.Info("[Simulated] 射线源已完全关闭");
|
||||
return XRayResult.Ok();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedLinearAxisTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MoveToTarget_SetsStatusToMoving_ThenIdle()
|
||||
{
|
||||
// Arrange - use fast speed for quicker test
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 200);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(50);
|
||||
|
||||
// Assert - should be Moving immediately after call
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(AxisStatus.Moving, axis.Status);
|
||||
|
||||
// Wait for move to complete (50mm at 200mm/s = 250ms, add generous buffer)
|
||||
await Task.Delay(1000);
|
||||
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.Equal(50.0, axis.ActualPosition, precision: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveToTarget_OutOfRange_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(150);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_DuringMove_KeepsCurrentPosition()
|
||||
{
|
||||
// Arrange - use slow speed so we can stop mid-move
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 10);
|
||||
|
||||
// Act - start a long move (100mm at 10mm/s = 10s)
|
||||
axis.MoveToTarget(100);
|
||||
await Task.Delay(200); // Let it move a bit
|
||||
|
||||
axis.Stop();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.True(axis.ActualPosition > 0, "Position should have moved from 0");
|
||||
Assert.True(axis.ActualPosition < 100, "Position should not have reached target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Home_MovesToZero()
|
||||
{
|
||||
// Arrange - move to 50 first with fast speed
|
||||
var axis = new SimulatedLinearAxis(AxisId.StageX, min: 0, max: 100, origin: 0, defaultSpeed: 200);
|
||||
axis.MoveToTarget(50);
|
||||
await Task.Delay(1000); // Wait for move to complete
|
||||
|
||||
Assert.Equal(50.0, axis.ActualPosition, precision: 1);
|
||||
|
||||
// Act
|
||||
axis.Home();
|
||||
await Task.Delay(1000); // Wait for home to complete
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0.0, axis.ActualPosition, precision: 1);
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedRotaryAxisTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MoveToTarget_SetsStatusToMoving_ThenIdle()
|
||||
{
|
||||
// Arrange - use fast speed for quicker test
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true, defaultSpeed: 200);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(90);
|
||||
|
||||
// Assert - should be Moving immediately after call
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(AxisStatus.Moving, axis.Status);
|
||||
|
||||
// Wait for move to complete (90° at 200°/s = 450ms, add generous buffer)
|
||||
await Task.Delay(1000);
|
||||
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.Equal(90.0, axis.ActualAngle, precision: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveToTarget_OutOfRange_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true);
|
||||
|
||||
// Act
|
||||
var result = axis.MoveToTarget(200);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_DuringMove_KeepsCurrentAngle()
|
||||
{
|
||||
// Arrange - use slow speed so we can stop mid-move
|
||||
var axis = new SimulatedRotaryAxis(
|
||||
RotaryAxisId.DetectorSwing, minAngle: -180, maxAngle: 180, enabled: true, defaultSpeed: 10);
|
||||
|
||||
// Act - start a long move (180° at 10°/s = 18s)
|
||||
axis.MoveToTarget(180);
|
||||
await Task.Delay(200); // Let it move a bit
|
||||
|
||||
axis.Stop();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AxisStatus.Idle, axis.Status);
|
||||
Assert.True(axis.ActualAngle > 0, "Angle should have moved from 0");
|
||||
Assert.True(axis.ActualAngle < 180, "Angle should not have reached target");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using Moq;
|
||||
using Prism.Events;
|
||||
using Xunit;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.RaySource.Abstractions.Enums;
|
||||
using XP.Hardware.RaySource.Abstractions.Events;
|
||||
using XP.Hardware.RaySource.Implementations;
|
||||
|
||||
namespace XplorePlane.Tests.Hardware
|
||||
{
|
||||
public class SimulatedXRaySourceTests
|
||||
{
|
||||
private readonly Mock<IEventAggregator> _mockEventAggregator;
|
||||
private readonly Mock<ILoggerService> _mockLogger;
|
||||
private readonly Mock<RaySourceStatusChangedEvent> _mockStatusEvent;
|
||||
private readonly SimulatedXRaySource _source;
|
||||
|
||||
public SimulatedXRaySourceTests()
|
||||
{
|
||||
_mockEventAggregator = new Mock<IEventAggregator>();
|
||||
_mockLogger = new Mock<ILoggerService>();
|
||||
_mockStatusEvent = new Mock<RaySourceStatusChangedEvent>();
|
||||
|
||||
// Setup logger to return itself for ForModule<T>()
|
||||
_mockLogger.Setup(l => l.ForModule<SimulatedXRaySource>()).Returns(_mockLogger.Object);
|
||||
|
||||
// Setup event aggregator to return the mock event
|
||||
_mockEventAggregator
|
||||
.Setup(ea => ea.GetEvent<RaySourceStatusChangedEvent>())
|
||||
.Returns(_mockStatusEvent.Object);
|
||||
|
||||
_source = new SimulatedXRaySource(_mockEventAggregator.Object, _mockLogger.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_SetsInitializedTrue()
|
||||
{
|
||||
// Act
|
||||
var initResult = _source.Initialize();
|
||||
var connectResult = _source.ConnectVariables();
|
||||
|
||||
// Assert
|
||||
Assert.True(initResult.Success);
|
||||
Assert.True(connectResult.Success);
|
||||
Assert.True(_source.IsConnected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurnOn_PublishesOpenedEvent()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
|
||||
// Act
|
||||
var result = _source.TurnOn();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
_mockStatusEvent.Verify(
|
||||
e => e.Publish(RaySourceStatus.Opened),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurnOff_PublishesClosedEvent()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.TurnOn();
|
||||
|
||||
// Act
|
||||
var result = _source.TurnOff();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
_mockStatusEvent.Verify(
|
||||
e => e.Publish(RaySourceStatus.Closed),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadVoltage_ReturnsWithinTwoPercentOfSetValue()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.SetVoltage(100f);
|
||||
|
||||
// Act & Assert - read multiple times to verify range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var result = _source.ReadVoltage();
|
||||
Assert.True(result.Success);
|
||||
|
||||
float voltage = result.GetFloat();
|
||||
Assert.InRange(voltage, 98f, 102f);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseOff_ResetsState()
|
||||
{
|
||||
// Arrange
|
||||
_source.Initialize();
|
||||
_source.ConnectVariables();
|
||||
_source.TurnOn();
|
||||
|
||||
Assert.True(_source.IsConnected);
|
||||
|
||||
// Act
|
||||
var result = _source.CloseOff();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.False(_source.IsConnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\XP.Common\XP.Common.csproj" />
|
||||
<ProjectReference Include="..\XP.Hardware.MotionControl\XP.Hardware.MotionControl.csproj" />
|
||||
<ProjectReference Include="..\XP.Hardware.RaySource\XP.Hardware.RaySource.csproj" />
|
||||
<ProjectReference Include="..\XplorePlane\XplorePlane.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
|
||||
<!-- 射线源配置 -->
|
||||
<!-- 射线源类型 | Ray Source Type -->
|
||||
<!-- 可选值: Comet225 | Available values: Comet225 -->
|
||||
<add key="RaySource:SourceType" value="Comet225" />
|
||||
<!-- 可选值: Comet225, Simulated | Available values: Comet225, Simulated -->
|
||||
<add key="RaySource:SourceType" value="Simulated" />
|
||||
<add key="RaySource:SerialNumber" value="SN08602861" />
|
||||
<add key="RaySource:TotalLifeThreshold" value="10" />
|
||||
<!-- PVI通讯参数 | PVI Communication Parameters -->
|
||||
@@ -130,6 +130,10 @@
|
||||
<!-- 自动重连 | Auto Reconnection -->
|
||||
<add key="Plc:bReConnect" value="true" />
|
||||
|
||||
<!-- 运动控制类型: PLC | Simulated | Motion control type -->
|
||||
<!-- 切换为 Simulated 可在无硬件环境下验证运动控制链路 | Switch to Simulated to verify motion control without hardware -->
|
||||
<add key="MotionControl:Type" value="Simulated" />
|
||||
|
||||
<!-- 直线轴配置(单位:mm)| Linear axis config (unit: mm) -->
|
||||
<add key="MotionControl:SourceZ:Min" value="-500" />
|
||||
<add key="MotionControl:SourceZ:Max" value="500" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -10,6 +11,10 @@ using System.Windows.Media.Imaging;
|
||||
using Prism.Events;
|
||||
using XP.Common.Converters;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
using XP.Hardware.MotionControl.Services;
|
||||
using XP.Hardware.RaySource.Services;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Helpers;
|
||||
using XplorePlane.Models;
|
||||
@@ -33,6 +38,9 @@ namespace XplorePlane.Services.Cnc
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IImagePersistenceService _imagePersistenceService;
|
||||
private readonly IMotionControlService _motionControlService;
|
||||
private readonly IMotionSystem _motionSystem;
|
||||
private readonly IRaySourceService _raySourceService;
|
||||
|
||||
// Task 4.2: volatile field so reads/writes are not reordered across threads
|
||||
private volatile CancellationTokenSource _executionCts;
|
||||
@@ -45,7 +53,10 @@ namespace XplorePlane.Services.Cnc
|
||||
IPipelineExecutionService pipelineExecutionService,
|
||||
IImageProcessingService imageProcessingService,
|
||||
IEventAggregator eventAggregator,
|
||||
IImagePersistenceService imagePersistenceService)
|
||||
IImagePersistenceService imagePersistenceService,
|
||||
IMotionControlService motionControlService = null,
|
||||
IMotionSystem motionSystem = null,
|
||||
IRaySourceService raySourceService = null)
|
||||
{
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
@@ -55,6 +66,9 @@ namespace XplorePlane.Services.Cnc
|
||||
_imageProcessingService = imageProcessingService;
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_imagePersistenceService = imagePersistenceService ?? throw new ArgumentNullException(nameof(imagePersistenceService));
|
||||
_motionControlService = motionControlService;
|
||||
_motionSystem = motionSystem;
|
||||
_raySourceService = raySourceService;
|
||||
|
||||
// Task 4.3: subscribe to DetectorDisconnectedEvent on a background thread
|
||||
_eventAggregator.GetEvent<DetectorDisconnectedEvent>()
|
||||
@@ -144,6 +158,7 @@ namespace XplorePlane.Services.Cnc
|
||||
if (linkedCts.Token.IsCancellationRequested)
|
||||
{
|
||||
cancelled = true;
|
||||
_motionControlService?.StopAll();
|
||||
_logger.ForModule<CncExecutionService>().Info(
|
||||
"Multi-position execution cancelled at position {0}/{1}",
|
||||
positionIndex, totalPositions);
|
||||
@@ -177,6 +192,37 @@ namespace XplorePlane.Services.Cnc
|
||||
string savedImagePath = null;
|
||||
string nodeErrorMessage = null;
|
||||
|
||||
// ── Step 0: Move to target position (motion integration) ──
|
||||
var moveResult = await MoveToPositionAsync(sp.MotionState, linkedCts.Token);
|
||||
if (!moveResult.Success)
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().Warn(
|
||||
"Motion move failed for node '{0}' at index {1}: {2}",
|
||||
sp.Name, positionIndex, moveResult.ErrorMessage);
|
||||
progress?.Report(new CncNodeExecutionProgress(
|
||||
sp.Id, NodeExecutionState.Failed,
|
||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
||||
allSucceeded = false;
|
||||
positionResults.Add(new PositionResult
|
||||
{
|
||||
NodeName = sp.Name,
|
||||
NodeIndex = sp.Index,
|
||||
Status = "Failed",
|
||||
ErrorMessage = $"Motion failed: {moveResult.ErrorMessage}"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for all axes to settle
|
||||
var settled = await WaitForAxesSettledAsync(linkedCts.Token);
|
||||
if (!settled)
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().Warn(
|
||||
"Axes did not settle within timeout for node '{0}' at index {1}",
|
||||
sp.Name, positionIndex);
|
||||
// Continue anyway - axes may be close enough
|
||||
}
|
||||
|
||||
// ── Step 1: Image Acquisition (with error tolerance - Task 5.4) ──
|
||||
BitmapSource positionImage = null;
|
||||
|
||||
@@ -584,6 +630,92 @@ namespace XplorePlane.Services.Cnc
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves all axes to the target position specified by the MotionState.
|
||||
/// MotionState positions are in micrometers (μm); ILinearAxis uses millimeters (mm).
|
||||
/// Returns MotionResult.Ok() if motion service is not available (graceful degradation).
|
||||
/// </summary>
|
||||
private Task<MotionResult> MoveToPositionAsync(MotionState target, CancellationToken ct)
|
||||
{
|
||||
if (_motionControlService == null)
|
||||
return Task.FromResult(MotionResult.Ok());
|
||||
|
||||
// Linear axes: convert μm → mm (divide by 1000.0)
|
||||
var stageXResult = _motionControlService.MoveToTarget(AxisId.StageX, target.StageX / 1000.0);
|
||||
if (!stageXResult.Success) return Task.FromResult(stageXResult);
|
||||
|
||||
var stageYResult = _motionControlService.MoveToTarget(AxisId.StageY, target.StageY / 1000.0);
|
||||
if (!stageYResult.Success) return Task.FromResult(stageYResult);
|
||||
|
||||
var sourceZResult = _motionControlService.MoveToTarget(AxisId.SourceZ, target.SourceZ / 1000.0);
|
||||
if (!sourceZResult.Success) return Task.FromResult(sourceZResult);
|
||||
|
||||
var detectorZResult = _motionControlService.MoveToTarget(AxisId.DetectorZ, target.DetectorZ / 1000.0);
|
||||
if (!detectorZResult.Success) return Task.FromResult(detectorZResult);
|
||||
|
||||
// Rotary axes: angles are already in degrees, no conversion needed
|
||||
var detectorSwingResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.DetectorSwing, target.DetectorSwing);
|
||||
if (!detectorSwingResult.Success) return Task.FromResult(detectorSwingResult);
|
||||
|
||||
var stageRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.StageRotation, target.StageRotation);
|
||||
if (!stageRotationResult.Success) return Task.FromResult(stageRotationResult);
|
||||
|
||||
var fixtureRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.FixtureRotation, target.FixtureRotation);
|
||||
if (!fixtureRotationResult.Success) return Task.FromResult(fixtureRotationResult);
|
||||
|
||||
return Task.FromResult(MotionResult.Ok());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls all axes until they are settled (Status == Idle).
|
||||
/// Returns true when all axes are idle, false on timeout (30s).
|
||||
/// Returns true immediately if motion system is not available (graceful degradation).
|
||||
/// </summary>
|
||||
private async Task<bool> WaitForAxesSettledAsync(CancellationToken ct)
|
||||
{
|
||||
if (_motionSystem == null)
|
||||
return true;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
const int pollIntervalMs = 50;
|
||||
const int timeoutMs = 30_000;
|
||||
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
bool allIdle = true;
|
||||
|
||||
foreach (var axis in _motionSystem.LinearAxes.Values)
|
||||
{
|
||||
if (axis.Status != AxisStatus.Idle)
|
||||
{
|
||||
allIdle = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allIdle)
|
||||
{
|
||||
foreach (var axis in _motionSystem.RotaryAxes.Values)
|
||||
{
|
||||
if (axis.Status != AxisStatus.Idle)
|
||||
{
|
||||
allIdle = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allIdle)
|
||||
return true;
|
||||
|
||||
await Task.Delay(pollIntervalMs, ct);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private BitmapSource TryGetSourceImage()
|
||||
{
|
||||
// ── 优先级 1:MainViewportService 中的手动图像或当前显示图像 ──
|
||||
|
||||
Reference in New Issue
Block a user