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; } } }