using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using TeamAAS.Global.Devices;
using TeamAAS.Motion.Models;
namespace TeamAAS.Motion.Motions
{
///
/// 仿真运动卡:无硬件环境下的全功能实现(联调/演示/单元测试用)。
///
public class SimulatedMotionCard : IMotionCard, IMasterFollowHost, IMotionSimulation, IMotionSafetyHost, IMotionIoHost
{
private readonly MotionDeviceConfig _cfg;
private int _axisCount;
private IAxis[] _axes = Array.Empty();
private Dictionary _axisMap = new Dictionary();
private Dictionary _hwAxes = new Dictionary();
private Dictionary _axisCfgs = new Dictionary();
private readonly MasterFollowSession _follow = new MasterFollowSession();
private readonly MotionSafetyGuard _guard;
private readonly bool[] _localDi = new bool[64];
private readonly bool[] _localDo = new bool[64];
private readonly bool[] _ecatDi = new bool[256];
private readonly bool[] _ecatDo = new bool[256];
private Timer _updateTimer;
private bool _isConnected;
private readonly object _lock = new object();
private const double UpdateIntervalMs = 10;
public string Name => _cfg.Name;
public bool IsConnected => _isConnected;
public int AxisCount => _axisCount;
public bool IsEstopActive => _guard.EstopActive;
public bool IsStopActive => _guard.StopActive;
internal MotionDeviceConfig DeviceConfig => _cfg;
public event EventHandler ConnectionStateChanged;
public event EventHandler ErrorOccurred;
public event EventHandler SafetyChanged;
public SimulatedMotionCard(MotionDeviceConfig cfg)
{
_cfg = cfg ?? new MotionDeviceConfig();
RebuildAxes();
_guard = new MotionSafetyGuard(
() => _cfg,
ReadDI,
OnSafetyEstopRise,
OnSafetyEstopHold,
OnSafetyStopRise);
_guard.Changed += (_, __) =>
{
try { SafetyChanged?.Invoke(this, EventArgs.Empty); }
catch { }
};
}
public bool AllowEnable(out string reason) => _guard.AllowEnable(out reason);
public bool AllowMotion(out string reason) => _guard.AllowMotion(out reason);
private bool RejectEnable()
{
if (_guard.AllowEnable(out var reason)) return false;
RaiseError(reason);
return true;
}
private bool RejectMotion()
{
if (_guard.AllowMotion(out var reason)) return false;
RaiseError(reason);
return true;
}
private void RaiseError(string message)
{
ErrorOccurred?.Invoke(this, new MotionErrorEventArgs(message));
}
private void OnSafetyEstopRise()
{
if (!_isConnected) return;
EmergencyStop();
}
private void OnSafetyEstopHold()
{
if (!_isConnected) return;
SimulatedAxis[] axes;
lock (_lock)
{
if (!_isConnected) return;
axes = _hwAxes.Values.ToArray();
}
foreach (var a in axes) a.Disable();
}
private void OnSafetyStopRise()
{
if (!_isConnected) return;
Stop(-1);
}
public bool CanSimulateDi => true;
public int GetDiCount(MotionIoBank bank)
=> bank == MotionIoBank.EtherCat
? ClampCount(_cfg.InputCount, _ecatDi.Length, 16)
: ClampCount(_cfg.LocalInputCount, _localDi.Length, 16);
public int GetDoCount(MotionIoBank bank)
=> bank == MotionIoBank.EtherCat
? ClampCount(_cfg.OutputCount, _ecatDo.Length, 16)
: ClampCount(_cfg.LocalOutputCount, _localDo.Length, 16);
private static int ClampCount(int configured, int max, int fallback)
{
if (configured <= 0) return Math.Min(fallback, max);
return Math.Min(configured, max);
}
private static bool[] BankDi(MotionIoBank bank, bool[] local, bool[] ecat)
=> bank == MotionIoBank.EtherCat ? ecat : local;
public bool ReadDI(int portIndex) => ReadDI(MotionIoBank.Local, portIndex);
public bool ReadDI(MotionIoBank bank, int index)
{
var bits = BankDi(bank, _localDi, _ecatDi);
if (index < 0 || index >= bits.Length) return false;
return bits[index];
}
public bool ReadDO(MotionIoBank bank, int index)
{
var bits = BankDi(bank, _localDo, _ecatDo);
if (index < 0 || index >= bits.Length) return false;
return bits[index];
}
public bool WriteDO(int portIndex, bool value) => WriteDO(MotionIoBank.Local, portIndex, value);
public bool WriteDO(MotionIoBank bank, int index, bool value)
{
var bits = BankDi(bank, _localDo, _ecatDo);
if (index < 0 || index >= bits.Length) return false;
bits[index] = value;
return true;
}
public bool WriteDI(MotionIoBank bank, int index, bool value)
{
var bits = BankDi(bank, _localDi, _ecatDi);
if (index < 0 || index >= bits.Length) return false;
bits[index] = value;
return true;
}
public void ReloadFromConfig()
{
lock (_lock)
{
_follow.Clear();
RebuildAxes();
}
}
public bool Open()
{
lock (_lock)
{
if (_isConnected)
{
EnsureTimerUnlocked();
return true;
}
_isConnected = true;
EnsureTimerUnlocked();
}
_guard.Start();
ConnectionStateChanged?.Invoke(this, new MotionConnectionStateChangedEventArgs(true));
return true;
}
public void Simulate(double deltaSeconds)
{
if (deltaSeconds <= 0) return;
SimulatedAxis[] axes;
lock (_lock)
{
if (!_isConnected) return;
EnsureTimerUnlocked();
axes = _hwAxes.Values.ToArray();
}
foreach (var axis in axes)
axis.Update(deltaSeconds);
}
public void FinishDiscreteMoves()
{
SimulatedAxis[] axes;
lock (_lock)
{
if (!_isConnected) return;
axes = _hwAxes.Values.ToArray();
}
double dt = 0;
foreach (var axis in axes)
dt = Math.Max(dt, axis.RemainingDiscreteSeconds());
if (dt > 0)
Simulate(dt + 1e-6);
else
{
foreach (var axis in axes)
axis.SnapDiscreteIfIdleVelocity();
}
}
private void EnsureTimerUnlocked()
{
if (!_isConnected) return;
if (_updateTimer != null) return;
_updateTimer = new Timer(UpdateAxes, null, 0, (int)UpdateIntervalMs);
}
public void Close()
{
_guard.Stop();
Timer timerToDispose = null;
lock (_lock)
{
if (!_isConnected) return;
_isConnected = false;
timerToDispose = _updateTimer;
_updateTimer = null;
_follow.Clear();
}
timerToDispose?.Dispose();
ConnectionStateChanged?.Invoke(this, new MotionConnectionStateChangedEventArgs(false));
}
void IMasterFollowHost.BeginSoloMasterFollow(int masterAxisNo, IEnumerable slaves)
=> _follow.Begin(masterAxisNo, slaves);
void IMasterFollowHost.ReleaseMasterFollow() => _follow.Release();
IDisposable IMasterFollowHost.SuspendFollowRelease() => _follow.Suspend();
internal MotionAxisConfig GetAxisConfig(int axisNo)
{
if (_axisCfgs.TryGetValue(axisNo, out var cfg) && cfg != null)
return cfg;
return new MotionAxisConfig { AxisNo = axisNo };
}
private void ReleaseFollowIfIndependent(int axis)
{
if (_follow.Suspended || _follow.IsReleasing) return;
if (axis >= 0 && TryCombined(axis, out _)) return;
_follow.Release();
}
public bool SetEnable(int axis, bool enable)
{
if (enable && RejectEnable()) return false;
if (!_isConnected) return false;
if (axis < 0)
{
foreach (var a in _hwAxes.Values) { if (enable) a.Enable(); else a.Disable(); }
return true;
}
if (TryCombined(axis, out var combined))
{
if (enable) combined.Enable(); else combined.Disable();
return true;
}
if (!_hwAxes.TryGetValue(axis, out var hw)) return false;
if (enable) hw.Enable(); else hw.Disable();
return true;
}
public bool Home(int axis, int mode = 0)
{
if (RejectMotion()) return false;
if (!_isConnected || !IsValidAxis(axis)) return false;
if (TryCombined(axis, out var combined))
{
combined.Home((HomeMode)mode);
return true;
}
ReleaseFollowIfIndependent(axis);
if (!_hwAxes.TryGetValue(axis, out var hw)) return false;
hw.Home((HomeMode)mode);
return true;
}
public bool IsHomed(int axis)
{
if (!IsValidAxis(axis)) return false;
return _axisMap[axis].IsHomed;
}
public bool MoveAbsolute(int axis, double position, double velocity)
{
if (RejectMotion()) return false;
if (!_isConnected || !IsValidAxis(axis)) return false;
if (TryCombined(axis, out var combined))
{
combined.Velocity = velocity;
combined.MoveTo(position);
return true;
}
ReleaseFollowIfIndependent(axis);
var a = _axisMap[axis];
var cfg = GetAxisConfig(axis);
a.Velocity = Math.Abs(cfg.ClampVelocity(velocity));
a.MoveTo(cfg.ClampPosition(position));
return true;
}
public bool MoveRelative(int axis, double distance, double velocity)
{
if (RejectMotion()) return false;
if (!_isConnected || !IsValidAxis(axis)) return false;
if (TryCombined(axis, out var combined))
{
combined.Velocity = velocity;
combined.MoveBy(distance);
return true;
}
ReleaseFollowIfIndependent(axis);
var a = _axisMap[axis];
var cfg = GetAxisConfig(axis);
a.Velocity = Math.Abs(cfg.ClampVelocity(velocity));
double target = cfg.ClampPosition(a.CommandPosition + distance);
a.MoveTo(target);
return true;
}
public bool JogStart(int axis, int direction, double velocity)
{
if (RejectMotion()) return false;
if (!_isConnected || !IsValidAxis(axis)) return false;
if (TryCombined(axis, out var combined))
{
combined.Jog((direction >= 0 ? 1 : -1) * Math.Abs(velocity));
return true;
}
ReleaseFollowIfIndependent(axis);
var dir = direction >= 0 ? 1 : -1;
var axisObj = _axisMap[axis];
double vel = GetAxisConfig(axis).ClampJog(dir * Math.Abs(velocity), axisObj.CommandPosition);
axisObj.Jog(vel);
return true;
}
public bool JogStop(int axis)
{
if (!IsValidAxis(axis)) return false;
return Stop(axis);
}
public bool Stop(int axis = -1)
{
if (!_isConnected) return false;
if (axis < 0)
{
_follow.Release();
foreach (var a in _hwAxes.Values) a.Stop();
return true;
}
if (!IsValidAxis(axis)) return false;
if (TryCombined(axis, out var combined))
{
combined.Stop();
return true;
}
ReleaseFollowIfIndependent(axis);
_axisMap[axis].Stop();
return true;
}
public AxisStatus GetAxisStatus(int axis)
{
if (!IsValidAxis(axis)) return null;
var a = _axisMap[axis];
return new AxisStatus
{
Index = axis,
Name = a.Name,
Position = a.ActualPosition,
Velocity = a.ActualVelocity,
IsMoving = a.IsMoving,
IsHomed = a.IsHomed,
IsEnabled = a.IsEnabled
};
}
public List GetAllAxisStatus()
{
var list = new List(_axes.Length);
foreach (var a in _axes)
list.Add(GetAxisStatus(a.AxisNo));
return list;
}
public IAxis GetAxis(int axisNo)
{
if (!IsValidAxis(axisNo))
throw new ArgumentOutOfRangeException(nameof(axisNo), "轴号超出范围");
return _axisMap[axisNo];
}
public IList GetAxes() => _axes.ToList();
public bool MoveLinear(double[] targetPositions, double velocity)
{
if (RejectMotion()) return false;
if (!_isConnected || targetPositions == null) return false;
_follow.Release();
var hw = _hwAxes.Values.OrderBy(a => a.AxisNo).ToList();
if (targetPositions.Length > hw.Count) return false;
for (int i = 0; i < targetPositions.Length; i++)
{
var cfg = GetAxisConfig(hw[i].AxisNo);
hw[i].Velocity = Math.Abs(cfg.ClampVelocity(velocity));
hw[i].MoveTo(cfg.ClampPosition(targetPositions[i]));
}
return true;
}
public bool MoveArc(double[] centerPoint, double[] targetPositions, double velocity)
=> MoveLinear(targetPositions, velocity);
public bool EmergencyStop()
{
if (!_isConnected) return false;
_follow.Clear();
foreach (var axis in _hwAxes.Values) axis.Stop();
return true;
}
private bool IsValidAxis(int axis) => _axisMap.ContainsKey(axis);
private bool TryCombined(int axis, out CombinedMotionAxis combined)
{
combined = null;
if (_axisMap.TryGetValue(axis, out var a) && a is CombinedMotionAxis c)
{
combined = c;
return true;
}
return false;
}
private void RebuildAxes()
{
_follow.Clear();
var oldHw = _hwAxes;
var cfgs = MotionAxisCatalog.GetEffective(_cfg);
_hwAxes = new Dictionary();
_axisCfgs = new Dictionary();
_axisMap = new Dictionary();
var view = new List(cfgs.Count);
foreach (var cfg in cfgs)
{
var axis = new SimulatedAxis(this, cfg.AxisNo)
{
Name = string.IsNullOrWhiteSpace(cfg.Name) ? $"Axis-{cfg.AxisNo}" : cfg.Name,
Velocity = cfg.Velocity,
Acceleration = cfg.Acceleration,
Deceleration = cfg.Deceleration,
Jerk = cfg.Jerk
};
if (oldHw != null && oldHw.TryGetValue(cfg.AxisNo, out var prev))
{
axis.SetPosition(prev.ActualPosition);
}
_hwAxes[cfg.AxisNo] = axis;
_axisMap[cfg.AxisNo] = axis;
_axisCfgs[cfg.AxisNo] = cfg;
axis.BindSafety(cfg);
}
foreach (var cfg in cfgs)
{
if (cfg.IsGrouped && _hwAxes.TryGetValue(cfg.AxisNo, out var master))
{
var combined = new CombinedMotionAxis(this, cfg, master, no =>
_hwAxes.TryGetValue(no, out var a) ? a : null);
_axisMap[cfg.AxisNo] = combined;
view.Add(combined);
}
else if (_axisMap.TryGetValue(cfg.AxisNo, out var hw))
{
view.Add(hw);
}
}
_axes = view.ToArray();
_axisCount = _axes.Length;
}
private void UpdateAxes(object state)
{
Simulate(UpdateIntervalMs / 1000.0);
}
}
public class SimulatedAxis : IAxis
{
private readonly SimulatedMotionCard _owner;
private readonly object _lock = new object();
private AxisState _state = AxisState.Off;
private bool _isEnabled;
private bool _isMoving;
private bool _isHomed;
private double _commandPos;
private double _actualPos;
private double _actualVel;
private double _targetPos;
private double _velocity = 100.0;
private double _accel = 500.0;
private double _decel = 500.0;
private double _jerk = 1000.0;
private double _jogVelocity;
private MotionAxisConfig _safety = new MotionAxisConfig();
public int AxisNo { get; }
public string Name { get; set; }
public AxisState State => _state;
public bool IsEnabled => _isEnabled;
public bool IsMoving => _isMoving;
public bool IsHomed => _isHomed;
public double CommandPosition => _commandPos;
public double ActualPosition => _actualPos;
public double ActualVelocity => _actualVel;
public double Velocity { get => _velocity; set => _velocity = value; }
public double Acceleration { get => _accel; set => _accel = value; }
public double Deceleration { get => _decel; set => _decel = value; }
public double Jerk { get => _jerk; set => _jerk = value; }
public event EventHandler MotionStarted;
public event EventHandler MotionCompleted;
public event EventHandler StateChanged;
public event EventHandler ErrorOccurred;
public SimulatedAxis(SimulatedMotionCard owner, int axisNo)
{
_owner = owner;
AxisNo = axisNo;
Name = $"Axis-{axisNo}";
}
internal void BindSafety(MotionAxisConfig cfg)
{
_safety = cfg ?? new MotionAxisConfig { AxisNo = AxisNo };
}
internal double RemainingDiscreteSeconds()
{
lock (_lock)
{
if (!_isMoving || _state != AxisState.DiscreteMotion) return 0;
if (_velocity < 1e-9) return 0;
return Math.Abs(_targetPos - _actualPos) / _velocity;
}
}
internal void SnapDiscreteIfIdleVelocity()
{
lock (_lock)
{
if (!_isMoving || _state != AxisState.DiscreteMotion) return;
_actualPos = _targetPos;
_commandPos = _targetPos;
_actualVel = 0;
_isMoving = false;
SetState(AxisState.StandStill);
OnMotionCompleted(new AxisMotionEventArgs(AxisNo, _targetPos, _actualPos));
}
}
internal void Update(double deltaTime)
{
lock (_lock)
{
if (!_isEnabled || _state == AxisState.Error || _state == AxisState.Off)
return;
if (_state == AxisState.ContinuousMotion && Math.Abs(_jogVelocity) > 0.0001)
{
double delta = _jogVelocity * deltaTime;
double next = _safety.ClampPosition(_actualPos + delta);
if (Math.Abs(next - _actualPos) < 1e-9)
{
_jogVelocity = 0;
_actualVel = 0;
SetState(AxisState.StandStill);
_isMoving = false;
OnMotionCompleted(new AxisMotionEventArgs(AxisNo, _actualPos, _actualPos));
return;
}
_commandPos = next;
_actualPos = next;
_actualVel = _jogVelocity;
return;
}
if (_state == AxisState.DiscreteMotion)
{
double remaining = _targetPos - _actualPos;
double distance = Math.Abs(remaining);
if (distance < 0.0001)
{
_actualPos = _targetPos;
_commandPos = _targetPos;
_actualVel = 0;
SetState(AxisState.StandStill);
_isMoving = false;
OnMotionCompleted(new AxisMotionEventArgs(AxisNo, _targetPos, _actualPos));
return;
}
double speed = Math.Sign(remaining) * Math.Max(_velocity, 1e-6);
double step = speed * deltaTime;
if (Math.Abs(step) > Math.Abs(remaining) || _velocity < 1e-9)
step = remaining;
_actualPos += step;
_commandPos = _actualPos;
_actualVel = deltaTime > 1e-12 ? step / deltaTime : 0;
}
}
}
private void StartMoveTo(double target)
{
if (_owner != null && !_owner.AllowMotion(out var reason))
{
OnError(reason);
return;
}
lock (_lock)
{
if (!_isEnabled) { OnError("轴未使能,无法运动"); return; }
if (_state == AxisState.Error) { OnError("轴处于错误状态"); return; }
_velocity = Math.Abs(_safety.ClampVelocity(_velocity));
_targetPos = _safety.ClampPosition(target);
_jogVelocity = 0;
if (Math.Abs(_targetPos - _actualPos) < 0.0001 || _velocity < 1e-9)
{
_actualPos = _targetPos;
_commandPos = _targetPos;
_actualVel = 0;
_isMoving = false;
SetState(AxisState.StandStill);
OnMotionCompleted(new AxisMotionEventArgs(AxisNo, _targetPos, _actualPos));
return;
}
SetState(AxisState.DiscreteMotion);
_isMoving = true;
OnMotionStarted(new AxisMotionEventArgs(AxisNo, _targetPos, _actualPos));
}
}
public void Enable()
{
if (_owner != null && !_owner.AllowEnable(out var reason))
{
OnError(reason);
return;
}
lock (_lock)
{
if (_isEnabled) return;
_isEnabled = true;
if (_state == AxisState.Off) SetState(AxisState.StandStill);
}
}
public void Disable()
{
lock (_lock)
{
if (!_isEnabled) return;
_isMoving = false;
_jogVelocity = 0;
SetState(AxisState.Off);
_isEnabled = false;
}
}
public void MoveTo(double position) => StartMoveTo(position);
public void MoveBy(double distance) => StartMoveTo(_actualPos + distance);
public void Jog(double velocity)
{
if (_owner != null && !_owner.AllowMotion(out var reason))
{
OnError(reason);
return;
}
lock (_lock)
{
if (!_isEnabled) { OnError("轴未使能,无法点动"); return; }
if (_state == AxisState.Error) { OnError("轴处于错误状态"); return; }
_jogVelocity = _safety.ClampJog(velocity, _actualPos);
if (Math.Abs(_jogVelocity) < 1e-9)
{
_isMoving = false;
_actualVel = 0;
SetState(AxisState.StandStill);
return;
}
SetState(AxisState.ContinuousMotion);
_isMoving = true;
_actualVel = _jogVelocity;
OnMotionStarted(new AxisMotionEventArgs(AxisNo, double.NaN, _actualPos));
}
}
public void Stop()
{
lock (_lock)
{
if (!_isMoving) return;
_jogVelocity = 0;
_actualVel = 0;
SetState(AxisState.StandStill);
_isMoving = false;
OnMotionCompleted(new AxisMotionEventArgs(AxisNo, _targetPos, _actualPos));
}
}
public void Home(HomeMode mode = HomeMode.Default)
{
if (_owner != null && !_owner.AllowMotion(out var reason))
{
OnError(reason);
return;
}
lock (_lock)
{
if (!_isEnabled) { OnError("轴未使能,无法回零"); return; }
_isMoving = false;
_jogVelocity = 0;
_actualVel = 0;
_actualPos = 0;
_commandPos = 0;
_targetPos = 0;
_isHomed = true;
SetState(AxisState.StandStill);
OnMotionCompleted(new AxisMotionEventArgs(AxisNo, 0, 0));
}
}
public void SetPosition(double position)
{
lock (_lock)
{
_actualPos = position;
_commandPos = position;
}
}
public void SetMotionParams(MotionParams parameters)
{
lock (_lock)
{
if (parameters == null) return;
_velocity = parameters.Velocity;
_accel = parameters.Acceleration;
_decel = parameters.Deceleration;
_jerk = parameters.Jerk;
}
}
private void SetState(AxisState newState)
{
if (_state == newState) return;
var old = _state;
_state = newState;
OnStateChanged(new AxisStateChangedEventArgs(AxisNo, old, newState));
}
protected virtual void OnMotionStarted(AxisMotionEventArgs e) => MotionStarted?.Invoke(this, e);
protected virtual void OnMotionCompleted(AxisMotionEventArgs e) => MotionCompleted?.Invoke(this, e);
protected virtual void OnStateChanged(AxisStateChangedEventArgs e) => StateChanged?.Invoke(this, e);
protected virtual void OnError(string message) => ErrorOccurred?.Invoke(this, new AxisErrorEventArgs(AxisNo, message));
}
public class SimulatedIOCard : IIOCard
{
private readonly MotionDeviceConfig _cfg;
private readonly bool[] _inputs;
private readonly bool[] _outputs;
public string Name => _cfg.Name;
public int InputCount => _inputs.Length;
public int OutputCount => _outputs.Length;
public bool IsConnected { get; private set; }
public SimulatedIOCard(MotionDeviceConfig cfg)
{
_cfg = cfg ?? new MotionDeviceConfig();
_inputs = new bool[Math.Max(1, _cfg.InputCount)];
_outputs = new bool[Math.Max(1, _cfg.OutputCount)];
}
public bool Open() { IsConnected = true; return true; }
public void Close() { IsConnected = false; }
public bool[] ReadAllInputs()
{
lock (_inputs) { return (bool[])_inputs.Clone(); }
}
public bool[] ReadAllOutputs()
{
lock (_outputs) { return (bool[])_outputs.Clone(); }
}
public bool WriteOutput(int index, bool value)
{
if (index < 0 || index >= _outputs.Length) return false;
lock (_outputs) { _outputs[index] = value; }
return true;
}
public bool WriteOutputs(bool[] values)
{
if (values == null) return false;
lock (_outputs)
{
int n = Math.Min(values.Length, _outputs.Length);
for (int i = 0; i < n; i++) _outputs[i] = values[i];
}
return true;
}
}
}