diff --git a/src/Moryx.ControlSystem/Cells/SessionExtensions.cs b/src/Moryx.ControlSystem/Cells/SessionExtensions.cs new file mode 100644 index 0000000..5221b32 --- /dev/null +++ b/src/Moryx.ControlSystem/Cells/SessionExtensions.cs @@ -0,0 +1,128 @@ +// Copyright (c) 2024, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using Moryx.AbstractionLayer.Recipes; +using Moryx.AbstractionLayer; +using Moryx.AbstractionLayer.Products; +using Moryx.ControlSystem.Recipes; +using System; +using Moryx.ControlSystem.Processes; + +namespace Moryx.ControlSystem.Cells +{ + /// + /// Extension methods on the to get product related information, activity details or just provide shortcuts based on the actual session type + /// + public static class SessionExtensions + { + /// + /// Extension method to get the from the of the + /// + /// Type of the that is expected. + /// The sesion to get the from + /// + /// The in the session, if the belongs to a + /// and the holds a ; + /// Otherwise returns null + /// + public static TProductInstance GetProductInstance(this Session session) where TProductInstance : ProductInstance + { + if (session.Process is not ProductionProcess process) return null; + + return process.ProductInstance as TProductInstance; + } + + /// + /// Modifies the of type + /// on the of the using the given + /// . + /// + /// The expected type of the product instance + /// The sessopm holding the product instance + /// The action to be executed on the product instance + /// + /// + /// ((var instance) => instance.MyProperty = 1) + /// ]]> + /// + /// + /// Thrown if the of the + /// does not hold a product instance of type + /// + /// Thrown if the of the + /// is no + public static TInstance ModifyProductInstance(this Session session, Action setter) + where TInstance : IProductInstance => session.Process.ModifyProductInstance(setter); + + /// + /// Tries to modifies the of type + /// on the of the using the given + /// . Returns false, if the + /// operation could not be executed. + /// + /// The expected type of the product instance + /// The sessopm holding the product instance + /// The action to be executed on the product instance + /// + /// + /// ((var instance) => instance.MyProperty = 1) + /// ]]> + /// + /// + public static bool TryModifyProductInstance(this Session session, Action setter) + where TInstance : IProductInstance => session.Process.TryModifyProductInstance(setter); + + /// + /// Extension method to get the from the + /// + /// Type of the that is expected. + /// The sesion to get the from + /// + /// The in the session, if the currently handles an + /// Activity of type ; Otherwise returns null + /// + public static TActivityType GetActivity(this Session session) where TActivityType : Activity + { + if (session is ActivityCompleted completed) + return completed.CompletedActivity as TActivityType; + if (session is ActivityStart start) + return start.Activity as TActivityType; + + return null; + } + + /// + /// Extension method to get the last from the + /// + /// Type of the that is expected. + /// The sesion to get the from + /// + /// The last in the session, if the currently handles an + /// Activity of type ; Otherwise returns null + /// + public static TActivityType GetLastActivity(this Session session) where TActivityType : Activity + => session.Process.LastActivity() as TActivityType; + + /// + /// Extension method to get the from the + /// + /// Type of the that is expected. + /// The session to get the from + /// + /// The target in the session, if it belongs to a + /// or holds an with a ; Otherwise returns null. + /// + public static TProductType GetProductType(this Session session) where TProductType : ProductType + { + if (session.Process.Recipe is ISetupRecipe setupRecipe) + return setupRecipe.TargetRecipe.Target as TProductType; + + if (session.Process.Recipe is IProductRecipe prodcutRecipe) + return prodcutRecipe.Target as TProductType; + + return default; + } + } +} diff --git a/src/Moryx.ControlSystem/Jobs/JobExtensions.cs b/src/Moryx.ControlSystem/Jobs/JobExtensions.cs new file mode 100644 index 0000000..9637573 --- /dev/null +++ b/src/Moryx.ControlSystem/Jobs/JobExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2024, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using Moryx.AbstractionLayer.Recipes; +using Moryx.ControlSystem.Recipes; +using Moryx.ControlSystem.Setups; + +namespace Moryx.ControlSystem.Jobs +{ + /// + /// Extensions methods for s. + /// + public static class JobExtensions + { + /// + /// Checks whether the is an + /// and gets the predicted failure count; otherwise returns 0. + /// + public static int CountPredictedFailures(this Job job) + { + if (job is IPredictiveJob predictiveJob) + return predictiveJob.PredictedFailures.Count; + + return 0; + } + + /// + /// Checks if the references an + /// + public static bool IsProduction(this Job job) => job.Recipe is IProductionRecipe; + + /// + /// Checks if the references an + /// + public static bool IsSetup(this Job job) => job.Recipe is ISetupRecipe; + + /// + /// Checks if the holds an + /// that is set to be executed + /// + public static bool IsPreparingSetup(this Job job) + => job.Recipe is ISetupRecipe setup && setup.Execution == SetupExecution.BeforeProduction; + + /// + /// Checks if the holds an + /// that is set to be executed + /// + public static bool IsCleaningUpSetup(this Job job) + => job.Recipe is ISetupRecipe setup && setup.Execution == SetupExecution.AfterProduction; + } +} diff --git a/src/Moryx.ControlSystem/Processes/IProcessExtensions.cs b/src/Moryx.ControlSystem/Processes/IProcessExtensions.cs new file mode 100644 index 0000000..56d30c8 --- /dev/null +++ b/src/Moryx.ControlSystem/Processes/IProcessExtensions.cs @@ -0,0 +1,70 @@ +using Moryx.AbstractionLayer.Products; +using Moryx.AbstractionLayer; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Moryx.ControlSystem.Processes +{ + /// + /// Extensions on an + /// + public static class IProcessExtensions + { + /// + /// Modifies the of type + /// on the using the given . + /// + /// The expected type of the product instance + /// The process holding the product instance + /// The action to be executed on the product instance + /// + /// + /// ((var instance) => instance.MyProperty = 1) + /// ]]> + /// + /// + /// Thrown if the given does + /// not hold a product instance of type + /// Thrown if the given + /// is no + public static TInstance ModifyProductInstance(this IProcess process, Action setter) + where TInstance : IProductInstance + { + if (process is not ProductionProcess productionProcess) + throw new InvalidOperationException($"Cannot modify an {nameof(IProductInstance)} on a process of type {process.GetType()}"); + if (productionProcess.ProductInstance is not TInstance instance) + throw new InvalidCastException($"Cannot cast {nameof(ProductionProcess.ProductInstance)} of type " + + $"{productionProcess?.ProductInstance?.GetType()} to {typeof(TInstance)}"); + setter.Invoke(instance); + return instance; + } + + /// + /// Tries to modifies the of type + /// on the using the given . Returns false, if the + /// operation could not be executed. + /// + /// The expected type of the product instance + /// The process holding the product instance + /// The action to be executed on the product instance + /// + /// + /// ((var instance) => instance.MyProperty = 1) + /// ]]> + /// + /// + public static bool TryModifyProductInstance(this IProcess process, Action setter) + where TInstance : IProductInstance + { + if (process is not ProductionProcess productionProcess) + return false; + if (productionProcess.ProductInstance is not TInstance instance) + return false; + setter.Invoke(instance); + return true; + } + } +} diff --git a/src/Moryx.ControlSystem/Processes/ProcessHolderExtensions.cs b/src/Moryx.ControlSystem/Processes/ProcessHolderExtensions.cs index b8dfc61..fa08f5f 100644 --- a/src/Moryx.ControlSystem/Processes/ProcessHolderExtensions.cs +++ b/src/Moryx.ControlSystem/Processes/ProcessHolderExtensions.cs @@ -11,121 +11,117 @@ namespace Moryx.ControlSystem.Processes /// public static class ProcessHolderExtensions { + #region Get Position /// - /// Get the position by number + /// Get the position by its /// - public static TPosition GetPositionByIdentifier(this IProcessHolderGroup group, string identifier) - where TPosition : IProcessHolderPosition - { - return GetPosition(group.Positions, t => t.Identifier == identifier); - } + public static TPosition GetPositionByIdentifier(this IProcessHolderGroup group, string identifier) + where TPosition : IProcessHolderPosition => GetPosition(group.Positions, t => t.Identifier == identifier); /// - /// Get the position by number + /// Get the position by its + /// + public static IProcessHolderPosition GetPositionByIdentifier(this IProcessHolderGroup group, string identifier) + => GetPosition(group.Positions, t => t.Identifier == identifier); + + /// + /// Get the position by its /// public static TPosition GetPositionByIdentifier(this IEnumerable positions, string identifier) - where TPosition : IProcessHolderPosition - { - return GetPosition(positions, t => t.Identifier == identifier); - } + where TPosition : IProcessHolderPosition => GetPosition(positions, t => t.Identifier == identifier); /// - /// Get the position by session + /// Get the position by its /// public static TPosition GetPositionBySession(this IProcessHolderGroup group, Session session) - where TPosition : IProcessHolderPosition - { - return GetPosition(group.Positions, t => t.Session?.Id == session.Id); - } + where TPosition : IProcessHolderPosition => GetPosition(group.Positions, t => t.Session?.Id == session.Id); + + /// + /// Get the position by its + /// + public static IProcessHolderPosition GetPositionBySession(this IProcessHolderGroup group, Session session) + => GetPosition(group.Positions, t => t.Session?.Id == session.Id); /// - /// Get the position by session + /// Get the position by its /// public static TPosition GetPositionBySession(this IEnumerable positions, Session session) - where TPosition : IProcessHolderPosition - { - return GetPosition(positions, t => t.Session?.Id == session.Id); - } + where TPosition : IProcessHolderPosition => GetPosition(positions, t => t.Session?.Id == session.Id); /// - /// Get the position by its process id + /// Get the position by its /// public static TPosition GetPositionByProcessId(this IProcessHolderGroup group, long processId) - where TPosition : IProcessHolderPosition - { - return GetPosition(group.Positions, t => t.Process?.Id == processId); - } + where TPosition : IProcessHolderPosition => GetPosition(group.Positions, t => t.Process?.Id == processId); + + /// + /// Get the position by its + /// + public static IProcessHolderPosition GetPositionByProcessId(this IProcessHolderGroup group, long processId) + => GetPosition(group.Positions, t => t.Process?.Id == processId); /// - /// Get the position by its process id + /// Get the position by its /// public static TPosition GetPositionByProcessId(this IEnumerable positions, long processId) - where TPosition : IProcessHolderPosition - { - return GetPosition(positions, t => t.Process?.Id == processId); - } + where TPosition : IProcessHolderPosition => GetPosition(positions, t => t.Process?.Id == processId); /// /// Get the position by id of the running activity /// public static TPosition GetPositionByActivityId(this IProcessHolderGroup group, long activityId) - where TPosition : IProcessHolderPosition - { - return GetPosition(group.Positions, t => (t.Session as ActivityStart)?.Activity.Id == activityId); - } + where TPosition : IProcessHolderPosition => GetPosition(group.Positions, t => (t.Session as ActivityStart)?.Activity.Id == activityId); /// - /// Checks if the group holds a process with a finished activity having the matching result - /// - public static bool HasFinishedActivity(this IProcessHolderGroup group, long activityId, long activityResult) - where TPosition : IProcessHolderPosition - { - return group.Positions.Any(position => position.HasFinishedActivity(activityId, activityResult)); - } - /// - /// Checks if the position holds a process with a finished activity having the matching result. + /// Get the position by id of the running activity /// - public static bool HasFinishedActivity(this IProcessHolderPosition holderPosition, long activityId, long activityResult) - { - return holderPosition.Process?.GetActivities(activity => activity.Id == activityId && activity.Result?.Numeric == activityResult).Any() == true; - } + public static IProcessHolderPosition GetPositionByActivityId(this IProcessHolderGroup group, long activityId) + => GetPosition(group.Positions, t => (t.Session as ActivityStart)?.Activity.Id == activityId); /// /// Get the position by id of the running activity /// public static TPosition GetPositionByActivityId(this IEnumerable positions, long activityId) - where TPosition : IProcessHolderPosition + where TPosition : IProcessHolderPosition => GetPosition(positions, t => (t.Session as ActivityStart)?.Activity.Id == activityId); + + /// + /// Tries to get an empty from the + /// + /// The group to search + /// When this method returns, contains the first empty + /// , if any is found; otherwise null + /// + /// true if an empty was found; otherwise, false. + /// + public static bool TryGetEmptyPosition(this IProcessHolderGroup group, out IProcessHolderPosition position) { - return GetPosition(positions, t => (t.Session as ActivityStart)?.Activity.Id == activityId); + foreach (var possible in group.Positions) + { + if (!possible.IsEmpty()) + continue; + position = possible; + return true; + } + position = default; + return false; } private static TPosition GetPosition(IEnumerable positions, Func filter) - where TPosition : IProcessHolderPosition - { - return positions.SingleOrDefault(filter); - } + where TPosition : IProcessHolderPosition => positions.SingleOrDefault(filter); + + #endregion + + #region Get or Create Sessions /// /// Try cast and return the holders session property /// public static TSession ConvertSession(this IProcessHolderPosition holderPosition) - where TSession : Session - { - return holderPosition.Session as TSession; - } - - /// - /// Access tracing of the current activity - /// - public static TTracing Tracing(this IProcessHolderPosition position) - where TTracing : Tracing, new() - { - var currentActivity = (position.Session as ActivityStart)?.Activity; - return currentActivity?.TransformTracing(); - } + where TSession : Session => holderPosition.Session as TSession; /// - /// Create sessions for all positions on a holder group, that have a process + /// Get or create a session for the , if it holds a process. Otherwise returns an empty enumerable. + /// This is usually used when attaching to the control system. /// public static IEnumerable Attach(this ProcessHolderPosition position) { @@ -136,43 +132,148 @@ public static IEnumerable Attach(this ProcessHolderPosition position) } /// - /// Create sessions for all positions on a holder group, that have a process + /// Get or create sessions for all that have a process. + /// This is usually used when attaching to the control system. /// - public static IEnumerable Attach(this IEnumerable positions) - { - return positions.SelectMany(p => p.Attach()); - } + public static IEnumerable Attach(this IEnumerable positions) + => positions.SelectMany(p => p.Attach()); /// - /// Create sessions for all positions on a holder group, that have a process + /// Get or create sessions for all that have a process. + /// This is usually used when attaching to the control system. /// - public static IEnumerable Detach(this ProcessHolderPosition position) + public static IEnumerable Attach(this IProcessHolderGroup group) { - return position.Session != null ? new[] { position.Session } : Enumerable.Empty(); + var sessions = new List(); + foreach(var position in group.Positions) + { + if (position.Session is not null) + { + sessions.Add(position.Session); + continue; + } + + if (position.Process is null) + continue; + + if (position is ProcessHolderPosition php) + sessions.Add(php.StartSession()); + } + return sessions; } /// - /// Create sessions for all positions on a holder group, that have a process + /// Get a session if has one. Otherwise returns an empty enumerable. + /// This is usually used when detaching from the control system. /// - public static IEnumerable Detach(this IEnumerable positions) - { - return positions.Where(p => p.Session != null).Select(p => p.Session); - } + public static IEnumerable Detach(this ProcessHolderPosition position) + => position.Session != null ? new[] { position.Session } : Enumerable.Empty(); /// - /// Assign process and session to this position + /// Gets the session from each in the + /// that holds one. This is usually used when detaching from the control system. /// - public static void Mount(this IProcessHolderPosition position, IProcess process) - { - position.Mount(new MountInformation(process, null)); - } + public static IEnumerable Detach(this IEnumerable positions) + => positions.Where(p => p.Session != null).Select(p => p.Session); + + /// + /// Gets the session from each in the + /// that holds one. This is usually used when detaching from the control system. + /// + public static IEnumerable Detach(this IProcessHolderGroup group) + => group.Positions.Where(p => p.Session != null).Select(p => p.Session); + + #endregion + + #region Mounting + + /// + /// Assign a to this position + /// + public static void Mount(this IProcessHolderPosition position, IProcess process) + => position.Mount(new MountInformation(process, null)); + + /// + /// Assign a to this position + /// + public static void Mount(this IProcessHolderPosition position, Session session) + => position.Mount(new MountInformation(null, session)); + + /// + /// Assign and to this position + /// + public static void Mount(this IProcessHolderPosition position, IProcess process, Session session) + => position.Mount(new MountInformation(process, session)); + + #endregion + + #region Status Checks + + /// + /// Checks if the has no empty + /// + public static bool IsFull(this IProcessHolderGroup group) + => group.Positions.All(position => !position.IsEmpty()); + + /// + /// Checks if the has no filled + /// + public static bool IsEmpty(this IProcessHolderGroup group) + => group.Positions.All(position => position.IsEmpty()); + + /// + /// True if the has neither a nor a + /// ; false otherwise. + /// + public static bool IsEmpty(this IProcessHolderPosition position) + => position.Process is null && position.Session is null; /// - /// Assign process and session to this position + /// Checks if the group holds a process with a finished activity having the matching result + /// + public static bool HasFinishedActivity(this IProcessHolderGroup group, long activityId, long activityResult) + where TPosition : IProcessHolderPosition => group.Positions.Any(position => position.HasFinishedActivity(activityId, activityResult)); + + /// + /// Checks if the position holds a process with a finished activity having the matching result. /// - public static void Mount(this IProcessHolderPosition position, IProcess process, Session session) + public static bool HasFinishedActivity(this IProcessHolderPosition holderPosition, long activityId, long activityResult) + => holderPosition.Process?.GetActivities(activity => activity.Id == activityId && activity.Result?.Numeric == activityResult).Any() == true; + + #endregion + + /// + /// Access tracing of the current activity + /// + public static TTracing Tracing(this IProcessHolderPosition position) + where TTracing : Tracing, new() => (position.Session as ActivityStart)?.Activity?.TransformTracing(); + + /// + /// Updates and by resetting the + /// and remounting the . For s + /// a direct update of the session and process is done on the object. + /// + /// The position to update + /// The updated session on the position + /// Thrown if the given does not match + /// the session on the + public static void Update(this IProcessHolderPosition position, Session session) { - position.Mount(new MountInformation(process, session)); + if (position.Session.Id != session.Id) + throw new InvalidOperationException($"Tried to update the {nameof(Session)} on an " + + $"{nameof(IProcessHolderPosition)} with a different session. Make sure to " + + $"{nameof(IProcessHolderPosition.Reset)} the {nameof(IProcessHolderPosition)} before " + + $"assigning a new {nameof(Session)}"); + + if (position is ProcessHolderPosition explicitPosition) + { + explicitPosition.Session = session; + explicitPosition.AssignProcess(session.Process); + return; + } + + position.Reset(); + position.Mount(session.Process, session); } } } \ No newline at end of file diff --git a/src/Moryx.ControlSystem/Recipes/IRecipeExtensions.cs b/src/Moryx.ControlSystem/Recipes/IRecipeExtensions.cs new file mode 100644 index 0000000..2611657 --- /dev/null +++ b/src/Moryx.ControlSystem/Recipes/IRecipeExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using Moryx.AbstractionLayer.Recipes; +using System; + +namespace Moryx.ControlSystem.Recipes +{ + /// + /// Extensions on s + /// + public static class IRecipeExtensions + { + /// + /// Gets a string concatinating order and operation number from an + /// or and empty string if none could be found. + /// + /// An that implements + /// or an targeting an + /// Separation char between order number and operation number + public static string GetOrderOperationString(this IRecipe recipe, string seperator = "-") + { + var target = (recipe is ISetupRecipe setup ? setup.TargetRecipe : recipe) as IOrderBasedRecipe; + return $"{target?.OrderNumber}{(target is null ? "" : seperator)}{target?.OperationNumber}"; + } + } +} diff --git a/src/Moryx.ControlSystem/VisualInstructions/VisualInstructorExtensions.cs b/src/Moryx.ControlSystem/VisualInstructions/VisualInstructorExtensions.cs index 5010ed0..326ff59 100644 --- a/src/Moryx.ControlSystem/VisualInstructions/VisualInstructorExtensions.cs +++ b/src/Moryx.ControlSystem/VisualInstructions/VisualInstructorExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Data.Common; using System.Linq; using System.Reflection; using Moryx.AbstractionLayer; @@ -22,7 +21,7 @@ public static class VisualInstructorExtensions /// public static long Display(this IVisualInstructor instructor, string title, IVisualInstructions parameter) { - return instructor.Display(new ActiveInstruction + return instructor.Display(new ActiveInstruction { Title = title, Instructions = parameter.Instructions @@ -55,6 +54,64 @@ public static long Display(this IVisualInstructor instructor, string title, Acti }); } + /// + /// Show a visual instruction text message + /// + /// The instructor to display the message + /// The sender of the message + /// The message to be displayed + /// The id of the instruction + public static long DisplayMessage(this IVisualInstructor instructor, string sender, string message) + { + return instructor.DisplayMessages(sender, [message]); + } + + /// + /// Show a set of messages as a visual instruction + /// + /// The instructor to display the messages + /// The sender of the message + /// The messages to be displayed + /// The id of the instruction + public static long DisplayMessages(this IVisualInstructor instructor, string sender, string[] messages) + { + var instructions = messages.Select(AsInstruction).ToArray(); + return instructor.Display(new ActiveInstruction + { + Title = sender, + Instructions = instructions + }); + } + + /// + /// Show a visual instruction text message + /// + /// The instructor to display the message + /// The sender of the message + /// The message to be displayed + /// Time after which the message will be cleared + public static void DisplayMessage(this IVisualInstructor instructor, string sender, string message, int autoClearMs) + { + instructor.DisplayMessages(sender, [message], autoClearMs); + } + + /// + /// Show a set of messages as a visual instruction + /// + /// The instructor to display the messages + /// The sender of the message + /// The messages to be displayed + /// Time after which the messages will be cleared + public static void DisplayMessages(this IVisualInstructor instructor, string sender, string[] messages, int autoClearMs) + { + var instructions = messages.Select(AsInstruction).ToArray(); + instructor.Display(new ActiveInstruction + { + Title = sender, + Instructions = instructions + }, autoClearMs); + } + /// /// Execute these instructions based on the given activity and report the result on completion /// Can (but must not) be cleared with the method @@ -74,7 +131,7 @@ public static long Execute(this IVisualInstructor instructor, string title, IVis /// Can (but must not) be cleared with the method /// /// Type of enum used for possible instruction results - public static long Execute(this IVisualInstructor instructor, string title, IVisualInstructions parameter, Action callback) where T: Enum + public static long Execute(this IVisualInstructor instructor, string title, IVisualInstructions parameter, Action callback) where T : Enum { return instructor.Execute( title,