Files

485 lines
19 KiB
C#
Raw Permalink Normal View History

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<MotionStartedEventArgs> MotionStarted;
public event EventHandler<MotionFinishedEventArgs> MotionFinished;
public async Task<MotionResult> 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<MotionResult> 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<MotionResult> 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<MotionResult> 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<MotionResult> 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<MotionResult> ExecuteProtectedMotionAsync(MotionOperation operation, double? targetPosition, int timeoutMilliseconds, CancellationToken cancellationToken, Action prepareAction, Func<Task<MotionErrorCode>> 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<MotionErrorCode> WaitForCommandAsync(Task<MotionErrorCode> 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<bool> 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<bool> 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;
}
}