using MwFramework.Device; using MwFramework.Device.Motion; using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace MainShell.Motion { public sealed class MotionController { public const int DefaultTimeoutMilliseconds = 30000; private const int PollingIntervalMilliseconds = 20; private const double DefaultInPositionTolerance = 0.001d; private readonly SemaphoreSlim _motionLock = new SemaphoreSlim(1, 1); public MotionController(IAxis axis) { Axis = axis ?? throw new ArgumentNullException(nameof(axis)); AxisFunc = axis as IAxisFunc ?? throw new ArgumentException("Axis does not support motion commands.", nameof(axis)); } private IAxis Axis { get; } private IAxisFunc AxisFunc { get; } public string AxisName => Axis.Name; public double CurrentPos => Axis.State != null ? Axis.State.ActualPos : Axis.GetPositionImmediate(); public bool IsBusy => Axis.State != null && Axis.State.Moving; public bool IsAlarm => Axis.State != null && (Axis.State.ALM || Axis.State.ServoFault || Axis.State.EMG); public bool InPos => Axis.State != null && Axis.State.Inpos; public event EventHandler MotionStarted; public event EventHandler MotionFinished; public async Task MoveAbsAsync(double targetPos, int timeoutMilliseconds = DefaultTimeoutMilliseconds, CancellationToken cancellationToken = default(CancellationToken), double? positionTolerance = null) { await _motionLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { return await ExecuteProtectedMotionAsync( MotionOperation.MoveAbs, targetPos, timeoutMilliseconds, cancellationToken, prepareAction: () => { EnsureMotionStateReady(); // EnsureWithinSoftLimit(targetPos); Axis.Param.AbsPos = targetPos; }, executeAsync: () => { return Task.FromResult(Axis.AbsoluteMove()); }, positionTolerance: positionTolerance).ConfigureAwait(false); } finally { _motionLock.Release(); } } public async Task MoveRelAsync(double distance, int timeoutMilliseconds = DefaultTimeoutMilliseconds, CancellationToken cancellationToken = default(CancellationToken), double? positionTolerance = null) { await _motionLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { var targetPos = CurrentPos + distance; return await ExecuteProtectedMotionAsync( MotionOperation.MoveRel, targetPos, timeoutMilliseconds, cancellationToken, prepareAction: () => { EnsureMotionStateReady(); EnsureWithinSoftLimit(targetPos); Axis.Param.IncPos = distance; }, executeAsync: () => AxisFunc.RelativeMoveAsync(), positionTolerance: positionTolerance).ConfigureAwait(false); } finally { _motionLock.Release(); } } public async Task JogAsync(Dir direction, int durationMilliseconds, int timeoutMilliseconds = DefaultTimeoutMilliseconds, CancellationToken cancellationToken = default(CancellationToken)) { if (durationMilliseconds <= 0) { throw new ArgumentOutOfRangeException(nameof(durationMilliseconds)); } await _motionLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { return await ExecuteProtectedMotionAsync( MotionOperation.Jog, null, timeoutMilliseconds, cancellationToken, prepareAction: () => { EnsureMotionStateReady(); if (timeoutMilliseconds != Timeout.Infinite && durationMilliseconds > timeoutMilliseconds) { throw new TimeoutException(string.Format("Jog timeout before completion. Axis: {0}", AxisName)); } }, executeAsync: async () => { EnsureNoError(AxisFunc.JogStart(direction, false), MotionOperation.Jog); await Task.Delay(durationMilliseconds, cancellationToken).ConfigureAwait(false); return AxisFunc.StopMove(); }).ConfigureAwait(false); } finally { _motionLock.Release(); } } public async Task StopAsync(int timeoutMilliseconds = 3000, CancellationToken cancellationToken = default(CancellationToken)) { await _motionLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { return await ExecuteProtectedMotionAsync( MotionOperation.Stop, null, timeoutMilliseconds, cancellationToken, prepareAction: null, executeAsync: async () => { var stopResult = AxisFunc.StopMove(); await Task.CompletedTask.ConfigureAwait(false); return stopResult; }, skipCommandCompletionWait: true, skipInPositionValidation: true).ConfigureAwait(false); } finally { _motionLock.Release(); } } public async Task HomeAsync(int timeoutMilliseconds = DefaultTimeoutMilliseconds, CancellationToken cancellationToken = default(CancellationToken)) { await _motionLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { return await ExecuteProtectedMotionAsync( MotionOperation.Home, null, timeoutMilliseconds, cancellationToken, prepareAction: EnsureMotionStateReady, executeAsync: () => Task.Run(() => AxisFunc.Home())).ConfigureAwait(false); } finally { _motionLock.Release(); } } private async Task ExecuteProtectedMotionAsync(MotionOperation operation, double? targetPosition, int timeoutMilliseconds, CancellationToken cancellationToken, Action prepareAction, Func> executeAsync, bool skipCommandCompletionWait = false, bool skipInPositionValidation = false, double? positionTolerance = null) { ValidateTimeout(timeoutMilliseconds); var startPosition = CurrentPos; var startedAt = DateTime.UtcNow; OnMotionStarted(new MotionStartedEventArgs(AxisName, operation, targetPosition, startPosition, startedAt)); Exception failure = null; var cancelled = false; MotionResult motionResult = null; var watch = Stopwatch.StartNew(); using (cancellationToken.Register(TryStop)) { try { cancellationToken.ThrowIfCancellationRequested(); prepareAction?.Invoke(); var commandTask = executeAsync != null ? executeAsync() : throw new InvalidOperationException("Motion execute delegate is null."); if (!skipCommandCompletionWait) { var result = await WaitForCommandAsync(commandTask, watch, timeoutMilliseconds, cancellationToken).ConfigureAwait(false); EnsureNoError(result, operation); } else { EnsureNoError(await commandTask.ConfigureAwait(false), operation); } await WaitForMotionCompletedAsync(operation, targetPosition, watch, timeoutMilliseconds, cancellationToken, skipInPositionValidation, positionTolerance).ConfigureAwait(false); } catch (OperationCanceledException ex) { cancelled = true; failure = ex; } catch (Exception ex) { failure = ex; } finally { var finishedAt = DateTime.UtcNow; motionResult = CreateMotionResult(operation, targetPosition, startPosition, CurrentPos, failure == null && !cancelled, cancelled, failure, startedAt, finishedAt); OnMotionFinished(new MotionFinishedEventArgs(motionResult)); } } return motionResult; } private MotionResult CreateMotionResult(MotionOperation operation, double? targetPosition, double startPosition, double endPosition, bool succeeded, bool cancelled, Exception exception, DateTime startedAtUtc, DateTime finishedAtUtc) { return new MotionResult(AxisName, operation, targetPosition, startPosition, endPosition, succeeded, cancelled, exception, exception?.Message, false, startedAtUtc, finishedAtUtc); } private async Task WaitForCommandAsync(Task commandTask, Stopwatch watch, int timeoutMilliseconds, CancellationToken cancellationToken) { if (commandTask == null) { throw new InvalidOperationException("Motion command task is null."); } while (!commandTask.IsCompleted) { cancellationToken.ThrowIfCancellationRequested(); EnsureMotionStateHealthyDuringExecution(); ThrowIfTimeoutExceeded(timeoutMilliseconds, watch); await Task.Delay(PollingIntervalMilliseconds, cancellationToken).ConfigureAwait(false); } return await commandTask.ConfigureAwait(false); } private async Task WaitForMotionCompletedAsync(MotionOperation operation, double? targetPosition, Stopwatch watch, int timeoutMilliseconds, CancellationToken cancellationToken, bool skipInPositionValidation, double? positionTolerance) { Task waitPositionTask = StartWaitPositionTask(watch, timeoutMilliseconds, cancellationToken); while (true) { cancellationToken.ThrowIfCancellationRequested(); EnsureMotionStateHealthyDuringExecution(); ThrowIfTimeoutExceeded(timeoutMilliseconds, watch); if (waitPositionTask.IsCompleted) { bool waitCompleted = await waitPositionTask.ConfigureAwait(false); if (!waitCompleted) { cancellationToken.ThrowIfCancellationRequested(); throw new TimeoutException(string.Format("WaitPosition failed. Axis: {0}", AxisName)); } if (IsTargetReached(operation, targetPosition, skipInPositionValidation, positionTolerance)) { return; } throw new InvalidOperationException(string.Format("Axis wait completed but target position was not reached. Axis: {0}", AxisName)); } await Task.Delay(PollingIntervalMilliseconds, cancellationToken).ConfigureAwait(false); } } private Task StartWaitPositionTask(Stopwatch watch, int timeoutMilliseconds, CancellationToken cancellationToken) { int remainingTimeoutMilliseconds = GetRemainingTimeoutMilliseconds(watch, timeoutMilliseconds); return Task.Run(() => AxisFunc.WaitPosition(remainingTimeoutMilliseconds, cancellationToken), cancellationToken); } private int GetRemainingTimeoutMilliseconds(Stopwatch watch, int timeoutMilliseconds) { if (timeoutMilliseconds == Timeout.Infinite) { return Timeout.Infinite; } long remainingMilliseconds = timeoutMilliseconds - watch.ElapsedMilliseconds; if (remainingMilliseconds <= 0) { return 1; } if (remainingMilliseconds > int.MaxValue) { return int.MaxValue; } return (int)remainingMilliseconds; } private bool IsTargetReached(MotionOperation operation, double? targetPosition, bool skipInPositionValidation, double? positionTolerance) { if (operation == MotionOperation.Stop) { return !IsBusy; } if(positionTolerance == null) { return true; } if (skipInPositionValidation || !targetPosition.HasValue) { return true; } if (InPos) { return true; } var tolerance = positionTolerance.HasValue && positionTolerance.Value > 0d ? positionTolerance.Value : DefaultInPositionTolerance; return Math.Abs(CurrentPos - targetPosition.Value) <= tolerance; } private void EnsureMotionStateReady() { var state = Axis.State; if (state == null) { throw new InvalidOperationException(string.Format("Axis state is unavailable. Axis: {0}", AxisName)); } if (!state.Servo) { throw new InvalidOperationException(string.Format("Axis servo is off. Axis: {0}", AxisName)); } if (state.ALM || state.ServoFault || state.EMG) { throw new InvalidOperationException(string.Format("Axis is in alarm state. Axis: {0}", AxisName)); } //if (!state.CanMove) //{ // throw new InvalidOperationException(string.Format("Axis cannot move. Axis: {0}", AxisName)); //} } private void EnsureMotionStateHealthyDuringExecution() { var state = Axis.State; if (state == null) { throw new InvalidOperationException(string.Format("Axis state is unavailable during motion. Axis: {0}", AxisName)); } if (state.ALM || state.ServoFault || state.EMG) { throw new InvalidOperationException(string.Format("Axis entered alarm state during motion. Axis: {0}", AxisName)); } if (!state.Servo) { throw new InvalidOperationException(string.Format("Axis servo turned off during motion. Axis: {0}", AxisName)); } //if (!Axis.IsMotionDone()) //{ // throw new InvalidOperationException(string.Format("Axis cannot continue motion. Axis: {0}", AxisName)); //} } private void EnsureWithinSoftLimit(double targetPos) { double softMel = 0d; if (AxisFunc.GetSoftMel(ref softMel) == MotionErrorCode.NoError && targetPos < softMel) { throw new InvalidOperationException(string.Format("Target position {0} is below the negative soft limit {1}. Axis: {2}", targetPos, softMel, AxisName)); } double softPel = 0d; if (AxisFunc.GetSoftPel(ref softPel) == MotionErrorCode.NoError && targetPos > softPel) { throw new InvalidOperationException(string.Format("Target position {0} is above the positive soft limit {1}. Axis: {2}", targetPos, softPel, AxisName)); } } private static void ValidateTimeout(int timeoutMilliseconds) { if (timeoutMilliseconds <= 0 && timeoutMilliseconds != Timeout.Infinite) { throw new ArgumentOutOfRangeException(nameof(timeoutMilliseconds)); } } private void ThrowIfTimeoutExceeded(int timeoutMilliseconds, Stopwatch watch) { if (timeoutMilliseconds != Timeout.Infinite && watch.ElapsedMilliseconds > timeoutMilliseconds) { TryStop(); throw new TimeoutException(string.Format("Motion timeout. Axis: {0}", AxisName)); } } private void EnsureNoError(MotionErrorCode errorCode, MotionOperation operation) { if (errorCode != MotionErrorCode.NoError) { throw new InvalidOperationException(string.Format("{0} failed on axis {1}. Error: {2}", operation, AxisName, errorCode)); } } private void TryStop() { try { AxisFunc.StopMove(); } catch { } } private void OnMotionStarted(MotionStartedEventArgs args) { MotionStarted?.Invoke(this, args); } private void OnMotionFinished(MotionFinishedEventArgs args) { MotionFinished?.Invoke(this, args); } } public enum MotionOperation { MoveAbs, MoveRel, Jog, Stop, Home } public sealed class MotionStartedEventArgs : EventArgs { public MotionStartedEventArgs(string axisName, MotionOperation operation, double? targetPosition, double startPosition, DateTime startedAtUtc) { AxisName = axisName; Operation = operation; TargetPosition = targetPosition; StartPosition = startPosition; StartedAtUtc = startedAtUtc; } public string AxisName { get; } public MotionOperation Operation { get; } public double? TargetPosition { get; } public double StartPosition { get; } public DateTime StartedAtUtc { get; } } public sealed class MotionFinishedEventArgs : EventArgs { public MotionFinishedEventArgs(MotionResult result) { Result = result ?? throw new ArgumentNullException(nameof(result)); } public MotionResult Result { get; } public string AxisName => Result.AxisName; public MotionOperation Operation => Result.Operation; public double? TargetPosition => Result.TargetPosition; public double EndPosition => Result.EndPosition; public bool Succeeded => Result.Succeeded; public bool Cancelled => Result.Cancelled; public Exception Exception => Result.Exception; public DateTime StartedAtUtc => Result.StartedAtUtc; public DateTime FinishedAtUtc => Result.FinishedAtUtc; } }