Singer Instruments PIXL Client
Example Integration

Below is a full example of an integration of the PIXL client in C# 7.0.

To test this client:

A full example showing how to create a client for the PIXL in C# 7.0.
Copy Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Grpc.Core;
using Sila2;
using SI.PIXL.Client.Enums;
using SI.PIXL.Client.Structs.CommandResponse.Workflows.ColonyDetection;
using SI.PIXL.Client.Structs.CommandResponse.Workflows.RandomColonyPicking;
using SI.PIXL.Client.Structs.CommandResponse.Workflows.Rearray;
using SI.PIXL.Client.Structs.Instructions;
using SI.PIXL.Client.Structs.PlateHandling;

// disable CS4014 'await' warnings as this is running in a static context
#pragma warning disable CS4014

namespace SI.PIXL.Client.Console
{
    class Program
    {
        #region Notes

        /*
            This example application is provided by Singer Instrument Company Limited with the intention of providing an example
            integration of the SI.PIXL.Client NuGet package for either:
                - .NET Framework 4.6.1 or above
                - .NET Core 2.0 or above
                - .NET Standard 2.0 or above
            For help please visit https://si-pixl-client-documentation.azurewebsites.net/Webframe.html or email technicalsupport@singerinstruments.com.
        */

        #endregion

        #region Constants

        /// <summary>
        /// Get the argument used when specifying the gRPC host and port.
        /// </summary>
        public const string SpecifyHostAndPortArgument = "-P";

        /// <summary>
        /// Get the argument to use if it is desired that debug information should be displayed.
        /// </summary>
        public const string DisplayDebugInformationArgument = "-D";

        /// <summary>
        /// Get a character that is used for line breaks.
        /// </summary>
        private static readonly char LineBreakCharacter = char.Parse("-");

        /// <summary>
        /// Get a character representing a child elements prefix.
        /// </summary>
        private const string ChildElementPrefex = "\t-";

        #endregion

        #region StaticFields

        /// <summary>
        /// Get or set if debugging information is displayed.
        /// </summary>
        private static bool displayDebuggingInformation;

        #endregion

        #region StaticProperties

        /// <summary>
        /// Get or set the pending plate requests.
        /// </summary>
        protected static PlateRequest[] PendingPlateRequests { get; set; }

        /// <summary>
        /// Get or set if debugging information is displayed.
        /// </summary>
        public static bool DisplayDebuggingInformation
        {
            get { return displayDebuggingInformation; }
            set
            {
                // if was displaying debugging information before this value changed unsubscribe from the event
                if (displayDebuggingInformation)
                    PIXL.DebugInformationReceived -= PIXL_DebugInformationReceived;

                displayDebuggingInformation = value;

                // if now displaying debugging information then subscribe to the event
                if (displayDebuggingInformation)
                    PIXL.DebugInformationReceived += PIXL_DebugInformationReceived;
            }
        }

        /// <summary>
        /// Get or set the PIXL client.
        /// </summary>
        protected static PIXLClient PIXL { get; set; }

        #endregion

        #region Connection

        /// <summary>
        /// Parse a host and a port.
        /// </summary>
        /// <param name="argument">The argument. Should be in the following format: -P[HOST:PORT]</param>
        /// <param name="host">The parsed host.</param>
        /// <param name="port">The parsed port.</param>
        /// <returns>The port number.</returns>
        private static bool TryParseHostAndPort(string argument, out string host, out int port)
        {
            // check for null or empty string
            if (string.IsNullOrEmpty(argument))
            {
                host = string.Empty;
                port = 0;
                return false;
            }

            // remove -P
            argument = argument.ToUpper().Replace(SpecifyHostAndPortArgument, string.Empty);

            // trim white space
            argument = argument.Trim();

            // split at :
            var arguments = argument.Split(":".ToCharArray());

            // if incorrect number of arguments
            if (arguments.Length != 2)
            {
                host = string.Empty;
                port = 0;
                return false;
            }

            // set host
            host = arguments[0];

            // try parse port
            return int.TryParse(arguments[1], out port);
        }

        #endregion

        #region Formatting

        /// <summary>
        /// Append a line break.
        /// </summary>
        /// <param name="character">The character to use for breaks.</param>
        public static void AppendLineBreak(char character)
        {
            // get width
            var width = Math.Max(0, Console.WindowWidth - 1);

            // set cursor position
            if (Console.CursorLeft != 0)
                Console.Write(Environment.NewLine);

            // append characters
            for (var i = 0; i < width; i++)
                Console.Write(character);

            // end line
            Console.Write("\n");
        }

        /// <summary>
        /// Log an error message.
        /// </summary>
        /// <param name="message">The error message.</param>
        private static void LogError(string message)
        {
            // log the error, in red
            Log(message, ConsoleColor.Red);
        }

        /// <summary>
        /// Log a information message.
        /// </summary>
        /// <param name="message">The information message.</param>
        private static void LogInfo(string message)
        {
            // log the info, in dark cyan
            Log(message, ConsoleColor.DarkCyan);
        }

        /// <summary>
        /// Log a debug message.
        /// </summary>
        /// <param name="message">The information message.</param>
        private static void LogDebugInfo(string message)
        {
            // log the debugging information, in dark magenta
            Log(message, ConsoleColor.DarkMagenta);
        }

        /// <summary>
        /// Log an information message.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <param name="color">The color to display the message in.</param>
        private static void Log(string message, ConsoleColor color)
        {
            // hold previous colour
            var previous = Console.ForegroundColor;

            // change colour
            Console.ForegroundColor = color;

            // append message
            Console.WriteLine(message);

            // revert to previous colour
            Console.ForegroundColor = previous;
        }

        #endregion

        #region Parsing

        /// <summary>
        /// Try to parse a plate type.
        /// </summary>
        /// <param name="input">The plate type.</param>
        /// <param name="type">The parsed type.</param>
        /// <returns>True if the plate type could be parsed, else false.</returns>
        private static bool TryParsePlateType(string input, out PlateTypes type)
        {
            // determine the plate type by parsing the input
            switch (input.ToUpper())
            {
                case ("PLUSPLATE"):
                case ("SBS"):
                    type = PlateTypes.PlusPlate;
                    return true;
                case ("PLUSPLATE6"):
                case ("SBS6"):
                    type = PlateTypes.PlusPlate_6;
                    return true;
                case ("PLUSPLATE12"):
                case ("SBS12"):
                    type = PlateTypes.PlusPlate_12;
                    return true;
                case ("PLUSPLATE24"):
                case ("SBS24"):
                    type = PlateTypes.PlusPlate_24;
                    return true;
                case ("PLUSPLATE48"):
                case ("SBS48"):
                    type = PlateTypes.PlusPlate_48;
                    return true;
                case ("PLUSPLATE96"):
                case ("SBS96"):
                    type = PlateTypes.PlusPlate_96;
                    return true;
                case ("PLUSPLATE384"):
                case ("SBS384"):
                    type = PlateTypes.PlusPlate_384;
                    return true;
                case ("PLUSPLATE1536"):
                case ("SBS1536"):
                    type = PlateTypes.PlusPlate_1536;
                    return true;
                case ("MTP6"):
                case ("MWP6"):
                    type = PlateTypes.MWP_6;
                    return true;
                case ("MTP12"):
                case ("MWP12"):
                    type = PlateTypes.MWP_12;
                    return true;
                case ("MTP24"):
                case ("MWP24"):
                    type = PlateTypes.MWP_24;
                    return true;
                case ("MTP48"):
                case ("MWP48"):
                    type = PlateTypes.MWP_48;
                    return true;
                case ("MTP96"):
                case ("MWP96"):
                    type = PlateTypes.MWP_96;
                    return true;
                case ("MTP384"):
                case ("MWP384"):
                    type = PlateTypes.MWP_384;
                    return true;
                case ("DWP6"):
                    type = PlateTypes.DeepMWP_6;
                    return true;
                case ("DWP12"):
                    type = PlateTypes.DeepMWP_12;
                    return true;
                case ("DWP24"):
                    type = PlateTypes.DeepMWP_24;
                    return true;
                case ("DWP48"):
                    type = PlateTypes.DeepMWP_48;
                    return true;
                case ("DWP96"):
                    type = PlateTypes.DeepMWP_96;
                    return true;
                case ("DWP384"):
                    type = PlateTypes.DeepMWP_384;
                    return true;
                case ("PCR96_200ul"):
                    type = PlateTypes.PCR_96_200ul;
                    return true;
                case ("PCR96_300ul"):
                    type = PlateTypes.PCR_96_300ul;
                    return true;
                case ("PCR384_40ul"):
                    type = PlateTypes.PCR_384_40ul;
                    return true;
                case ("P90"):
                case ("PETRI90"):
                    type = PlateTypes.Petri_90;
                    return true;
                case ("P150"):
                case ("PETRI150"):
                    type = PlateTypes.Petri_150;
                    return true;
                default:
                    type = PlateTypes.None;
                    return false;
            }
        }

        /// <summary>
        /// Try and parse a plate role.
        /// </summary>
        /// <param name="input">The plate role.</param>
        /// <param name="role">The parsed role.</param>
        /// <returns>True if the plate role could be parsed, else false.</returns>
        private static bool TryParsePlateRole(string input, out PlateRoles role)
        {
            // determine the plate role by parsing the input
            switch (input.ToUpper())
            {
                case ("SOURCE"):
                case ("S"):
                    role = PlateRoles.Source;
                    return true;
                case ("TARGET"):
                case ("T"):
                    role = PlateRoles.Target;
                    return true;
                default:
                    role = PlateRoles.None;
                    return false;
            }
        }

        /// <summary>
        /// Try and parse a bay.
        /// </summary>
        /// <param name="input">The bay.</param>
        /// <param name="bay">The parsed bay.</param>
        /// <returns>True if the bay could be parsed, else false.</returns>
        private static bool TryParseBay(string input, out Bays bay)
        {
            // determine the bay by parsing the input
            switch (input.ToUpper())
            {
                case ("BLACK"):
                case ("BL"):
                case ("S"):
                case ("SOURCE"):
                    bay = Bays.Black;
                    return true;
                case ("RED"):
                case ("T1"):
                case ("R"):
                    bay = Bays.Red;
                    return true;
                case ("BLUE"):
                case ("T2"):
                case ("B"):
                    bay = Bays.Blue;
                    return true;
                case ("YELLOW"):
                case ("T3"):
                case ("Y"):
                    bay = Bays.Yellow;
                    return true;
                case ("GREEN"):
                case ("G"):
                case ("T4"):
                    bay = Bays.Green;
                    return true;
                default:
                    bay = Bays.None;
                    return false;
            }
        }

        #endregion

        #region InputProcessing

        /// <summary>
        /// Process user input.
        /// </summary>
        /// <param name="input">The user input.</param>
        /// <param name="command">The derived command.</param>
        /// <param name="arguments">The derived arguments.</param>
        private static void ProcessUserInput(string input, out string command, out string[] arguments)
        {
            // check something
            if (string.IsNullOrEmpty(input))
            {
                command = string.Empty;
                arguments = new string[0];
                return;
            }

            // split at spaces
            var splitInput = input.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);

            // if nothing, just a command with no arguments
            if (splitInput.Length == 0)
            {
                command = input;
                arguments = new string[0];
                return;
            }

            // set command
            command = splitInput[0];

            // set arguments
            var argumentsAsList = new List<string>();

            // set all arguments
            for (var i = 1; i < splitInput.Length; i++)
                argumentsAsList.Add(splitInput[i]);

            // set arguments
            arguments = argumentsAsList.ToArray();
        }

        /// <summary>
        /// Process input.
        /// </summary>
        private static void ProcessInput()
        {
            var continueExecution = true;

            while (continueExecution)
            {
                try
                {
                    // line break
                    AppendLineBreak(LineBreakCharacter);

                    // ensure console colour is correct
                    Console.ForegroundColor = ConsoleColor.White;

                    // prompt for command
                    Console.WriteLine("Enter a command: ");

                    // read input
                    var input = Console.ReadLine()?.ToUpper() ?? string.Empty;

                    // process the input
                    ProcessUserInput(input, out var command, out var arguments);

                    // select the command
                    switch (command)
                    {
                        case ("ABORT"):

                            // abort any running command
                            var abortResult = PIXL.Abort();

                            // log info
                            LogInfo(abortResult.ToString());

                            break;

                        case ("AWAKEN"):

                            // awaken the PIXL
                            var awakenResult = PIXL.Awaken();

                            // log info
                            LogInfo(awakenResult.ToString());

                            break;

                        case ("BAYSTATUS"):

                            // iterate bays
                            foreach (var bay in new[] { Bays.Black, Bays.Red, Bays.Blue, Bays.Yellow, Bays.Green })
                            {
                                // check if occupied
                                if (PIXL.IsBayOccupied(bay))
                                {
                                    // get plate
                                    var plate = PIXL.GetPlateLoadedInBay(bay);

                                    // display the contents of the bay
                                    LogInfo($"{bay}: {plate}.");
                                }
                                else
                                {
                                    // display that bay is empty
                                    LogInfo($"{bay}: Empty.");
                                }
                            }

                            break;

                        case ("DETERMINEPOSITION"):

                            // get position
                            var position = PIXL.DetermineRandomColonyPickingStartPosition(arguments[0], arguments[1], arguments[2]);

                            // display as info in the console
                            LogInfo($"Is available: {position.IsPositionAvailable}.");
                            LogInfo($"Row: {position.TargetLayoutStartRow}.");
                            LogInfo($"Column: {position.TargetLayoutStartColumn}.");

                            break;

                        case ("EXIT"):

                            // break execution
                            continueExecution = false;

                            break;

                        case ("HELP"):

                            // display help
                            DisplayCommandsList();

                            break;

                        case ("INFO"):

                            // display info
                            DisplayInfo();

                            break;

                        case ("INITIALISE"):

                            // initialise and log result
                            PIXL.Initialise(x => LogInfo(x.ToString()));

                            break;

                        case ("LISTPLATES"):

                            // display available plates
                            DisplayAvailablePlates();

                            break;

                        case ("LISTPROFILES"):

                            // display available pinning profiles
                            DisplayAvailablePinningProfiles();

                            break;

                        case ("LISTTEMPLATES"):

                            // display available project templates
                            DisplayAvailableProjectTemplates();
                            break;

                        case ("LOADED"):

                            // if there are no arguments
                            if (arguments.Length > 0)
                            {
                                /* expected arguments:
                                    0 - bay
                                    1 - plate ID
                                    2 - plate type
                                    3 - plate role
                                */

                                // try and parse the bay
                                if (!TryParseBay(arguments[0], out var loadBay))
                                    throw new Exception("Bay could not be identified from the input.");

                                // try and parse the plate type
                                if (!TryParsePlateType(arguments[2], out var loadType))
                                    throw new Exception("Plate type could not be identified from the input.");

                                // try and parse the plate role
                                if (!TryParsePlateRole(arguments[3], out var loadRole))
                                    throw new Exception("Plate role could not be identified from the input.");

                                // notify PIXL plate is loaded
                                var plateLoadedResult = PIXL.PlateLoaded(loadBay, arguments[1], loadType, loadRole, string.Empty);

                                // log result
                                LogInfo(plateLoadedResult.ToString());
                            }
                            else
                            {
                                // get loads and swaps
                                var pendingLoadAndSwaps = PendingPlateRequests?.Where(x => ((x.Action == PlateActions.Load) || (x.Action == PlateActions.Swap)));

                                // iterate all pending loads and swaps
                                foreach (var loadOrSwap in pendingLoadAndSwaps)
                                {
                                    // notify PIXL plate is loaded
                                    var plateLoadedResult = PIXL.PlateLoaded(loadOrSwap.Bay, loadOrSwap.Plate.ID, loadOrSwap.Plate.Type, loadOrSwap.Plate.Role, string.Empty);

                                    // log result
                                    LogInfo(plateLoadedResult.ToString());
                                }
                            }

                            break;

                        case ("MOTORISEDDOORCLOSE"):

                            // close the motorised door and log the result
                            PIXL.CloseMotorisedDoor(x => LogInfo(x.ToString()));

                            break;

                        case ("MOTORISEDDOOROPEN"):

                            // open the motorised door and log the result
                            PIXL.OpenMotorisedDoor(x => LogInfo(x.ToString()));

                            break;

                        case ("REMOVED"):

                            // if some arguments
                            if (arguments.Length > 0)
                            {
                                // try and parse the bay
                                if (!TryParseBay(arguments[0], out var removeBay))
                                    throw new Exception("Bay could not be identified from the input.");

                                // notify PIXL plates are removed
                                var removedResult = PIXL.PlateRemoved(removeBay);

                                // log result
                                LogInfo(removedResult.ToString());
                            }
                            else
                            {
                                // get pending removes
                                var pendingRemoves = PendingPlateRequests?.Where(x => x.Action == PlateActions.Remove);

                                // iterate all pending removes, 
                                foreach (var remove in pendingRemoves)
                                {
                                    // notify PIXL that plate has been removed
                                    var removedResult = PIXL.PlateRemoved(remove.Bay);

                                    // log result
                                    LogInfo(removedResult.ToString());
                                }
                            }

                            break;

                        case ("REMOVEDALL"):

                            // remove all plates from all bays
                            foreach (var bay in new[] { Bays.Black, Bays.Red, Bays.Blue, Bays.Yellow, Bays.Green })
                            {
                                // notify PIXL that plate has been removed
                                var removedResult = PIXL.PlateRemoved(bay);

                                // log result
                                LogInfo(removedResult.ToString());
                            }

                            break;

                        case ("RESET"):

                            // reset the PIXL and log the result
                            PIXL.Reset(x => LogInfo(x.ToString()));

                            break;

                        case ("RESTART"):

                            // restart the PIXL
                            var restartResult = PIXL.Restart();

                            // log result
                            LogInfo(restartResult.ToString());

                            break;

                        case ("RUNCD"):

                            // run the Colony Detection workflow
                            PIXL.RunColonyDetectionWorkflow("ID1234", string.Empty, OnRunColonyDetectionWorkflowResponseCallback);

                            break;

                        case ("RUNRCP"):

                            // run the Random Colony Picking workflow
                            PIXL.RunRandomColonyPickingWorkflow("ID1234", string.Empty, "", PIXLClient.NoColonyLimit, OnRunRandomColonyPickingWorkflowResponseCallback);

                            break;

                        case ("RUNREARRAY"):

                            // create some test plates
                            var plate1 = new Plate("Source_Plate_1", PlateTypes.PlusPlate_96, PlateRoles.Source);
                            var plate2 = new Plate("Target_Plate_1", PlateTypes.PlusPlate_96, PlateRoles.Target);

                            // create some test pinnings
                            var pinnings = new[]
                            {
                                new ConsecutivePinningInstruction(new[]
                                {
                                    new PinningInstruction(plate1, 1, 1),
                                    new PinningInstruction(plate2, 1, 1)
                                })
                            };

                            // run the Re-array workflow
                            PIXL.RunRearrayWorkflow("ID1234", string.Empty, string.Empty, pinnings, OnRunRearrayWorkflowResponseCallback);

                            break;

                        case ("RUNREARRAYFILE"):

                            // check arguments
                            if (arguments.Length == 0)
                                throw new Exception("Please specify the [PATH] and [ITERATIONS] as the input.");

                            // hold iterations
                            var iterations = 1;

                            // if more than just path specified
                            if (arguments.Length > 1)
                            {
                                // parse minutes, if specified as an argument
                                if (!int.TryParse(arguments[1], out iterations))
                                    throw new Exception("Iterations could not be identified from the input.");
                            }

                            // create thread for background
                            var thread = new Thread(() =>
                            {
                                // iterate all required cycles
                                for (var i = 0; i < iterations; i++)
                                {
                                    // display result
                                    LogInfo($"Beginning re-array cycle {i + 1} of {iterations}.");

                                    // run the Re-array workflow
                                    var result = PIXL.RunRearrayWorkflowFromFile("ID1234", arguments[0]).Result;

                                    // display result
                                    LogInfo($"Re-array cycle {i + 1} of {iterations} finished, result: {result.Result}");

                                    // break if aborted
                                    if ((PIXL.OperationalStatus.CommandStatus == CommandStatus.Aborted) || (PIXL.OperationalStatus.CommandStatus == CommandStatus.Aborting))
                                        break;

                                    // break if failed
                                    if (!result.Result.IsSuccess)
                                        break;
                                }
                            });

                            // start operation
                            thread.Start();

                            break;

                        case ("RUNUV"):

                            // parse minutes, if specified as an argument
                            if (!int.TryParse(arguments[0], out var minutes))
                                throw new Exception("Minutes could not be identified from the input.");

                            // run sterilisation and log the result
                            PIXL.RunUVSterilisation(minutes, x => LogInfo(x.ToString()));

                            break;

                        case ("SLEEP"):

                            // put the PIXL to sleep
                            var sleepResult = PIXL.Sleep();

                            // log result
                            LogInfo(sleepResult.ToString());

                            break;

                        case ("SHUTDOWN"):

                            // shutdown the PIXL
                            var shutdownResult = PIXL.ShutDown();

                            // log result
                            LogInfo(shutdownResult.ToString());

                            break;

                        default:

                            // display the default command or unknown command prompts
                            if (string.IsNullOrEmpty(command))
                                LogError("Please enter a command or type HELP to view command list.");
                            else
                                LogError($"{command} is an unrecognized command.");

                            break;
                    }
                }
                catch (RpcException e)
                {
                    // handle RCP exceptions
                    HandleRPCException(e);
                }
                catch (Exception e)
                {
                    // log general exceptions
                    LogError($"Exception caught processing input: {e.Message}");
                }
            }
        }

        #endregion

        #region Callbacks

        /// <summary>
        /// Handle a Run Random Colony Picking command response.
        /// </summary>
        /// <param name="response">The command response.</param>
        private static void OnRunRandomColonyPickingWorkflowResponseCallback(RunRandomColonyPickingCommandResponse response)
        {
            // format and display the output from the Random Colony Picking workflow on the console

            AppendLineBreak(LineBreakCharacter);

            // display program info
            LogInfo(response.Result.ToString());
            LogInfo($"Tracking base: {response.TrackingPath}");
            LogInfo($"Program ID: {response.ProgramInformation.ProgramID}");

            AppendLineBreak(LineBreakCharacter);
            LogInfo("Colonies:");

            var stringBuilder = new StringBuilder();

            // iterate and display all colony information
            for (var i = 0; i < response.ProgramInformation.ColonyInformation.Length; i++)
            {
                var colony = response.ProgramInformation.ColonyInformation[i];
                stringBuilder.AppendLine($"---{i + 1} of {response.ProgramInformation.ColonyInformation.Length}:");
                stringBuilder.AppendLine($"------ID: {colony.ID}");
                stringBuilder.AppendLine($"------Name: {colony.Name}");
                stringBuilder.AppendLine($"------IsSelected: {colony.IsSelected}");
                stringBuilder.AppendLine($"------SectorID: {colony.SectorID}");
                stringBuilder.AppendLine($"------X: {colony.X}");
                stringBuilder.AppendLine($"------Y: {colony.Y}");
                stringBuilder.AppendLine($"------Area: {colony.Area}");
                stringBuilder.AppendLine($"------Diameter: {colony.Diameter}");
                stringBuilder.AppendLine($"------Brightness: {colony.Brightness}");
                stringBuilder.AppendLine($"------AverageRed: {colony.AverageRed}");
                stringBuilder.AppendLine($"------AverageGreen: {colony.AverageGreen}");
                stringBuilder.AppendLine($"------AverageBlue: {colony.AverageBlue}");
                stringBuilder.AppendLine($"------Redness: {colony.Redness}");
                stringBuilder.AppendLine($"------Greenness: {colony.Greenness}");
                stringBuilder.AppendLine($"------Blueness: {colony.Blueness}");
                stringBuilder.AppendLine($"------ProximityToClosest: {colony.ProximityToClosest}");
                stringBuilder.AppendLine($"------Circularity: {colony.Circularity}");
            }

            if (response.ProgramInformation.ColonyInformation.Length == 0)
                stringBuilder.AppendLine("---None");

            LogInfo(stringBuilder.ToString());
            stringBuilder.Clear();

            AppendLineBreak(LineBreakCharacter);
            LogInfo("Pinnings:");

            // iterate and display all pinning information
            for (var i = 0; i < response.ProgramInformation.Pinnings.Length; i++)
            {
                var pinning = response.ProgramInformation.Pinnings[i];
                stringBuilder.AppendLine($"---{i + 1} of {response.ProgramInformation.Pinnings.Length}: ");
                stringBuilder.AppendLine($"------ID: {pinning.PinningID}");
                stringBuilder.AppendLine($"------Date: {pinning.DateTime.Day}/{pinning.DateTime.Month}/{pinning.DateTime.Year}");
                stringBuilder.AppendLine($"------Time: {pinning.DateTime.Hour}:{pinning.DateTime.Minute}:{pinning.DateTime.Second}");
                stringBuilder.AppendLine($"---------Source: {1} of {1}");
                stringBuilder.AppendLine($"------------Plate ID: {pinning.Source.PlateID}");
                stringBuilder.AppendLine($"------------Plate Name: {pinning.Source.PlateName}");
                stringBuilder.AppendLine($"------------Colony ID: {pinning.Source.ColonyID}");
                stringBuilder.AppendLine($"------------Colony Name: {pinning.Source.ColonyName}");
                stringBuilder.AppendLine($"------------X: {pinning.Source.X}");
                stringBuilder.AppendLine($"------------Y: {pinning.Source.Y}");
                stringBuilder.AppendLine($"------------Result: {pinning.Source.Result}");

                for (var j = 0; j < pinning.Targets.Length; j++)
                {
                    var target = pinning.Targets[j];
                    stringBuilder.AppendLine($"---------Target {j + 1} of {pinning.Targets.Length}:");
                    stringBuilder.AppendLine($"------------Plate ID: {target.PlateID}");
                    stringBuilder.AppendLine($"------------Plate Name: {target.PlateName}");
                    stringBuilder.AppendLine($"------------Location: {target.Location}");
                    stringBuilder.AppendLine($"------------X: {target.X}");
                    stringBuilder.AppendLine($"------------Y: {target.Y}");
                    stringBuilder.AppendLine($"------------Result: {target.Result}");
                }
            }

            if (response.ProgramInformation.Pinnings.Length == 0)
                stringBuilder.AppendLine("-None");

            LogInfo(stringBuilder.ToString());
            AppendLineBreak(LineBreakCharacter);
        }

        /// <summary>
        /// Handle a Run re-array workflow command response.
        /// </summary>
        /// <param name="response">The command response.</param>
        private static void OnRunRearrayWorkflowResponseCallback(RunRearrayCommandResponse response)
        {
            // format and display the output from the Random Colony Picking workflow on the console

            AppendLineBreak(LineBreakCharacter);

            // display program info
            LogInfo(response.Result.ToString());
            LogInfo($"Tracking base: {response.TrackingPath}");
            LogInfo($"Program ID: {response.ProgramInformation.ProgramID}");
            AppendLineBreak(LineBreakCharacter);

            var stringBuilder = new StringBuilder();

            LogInfo("Pinnings:");

            // iterate and display all colony information
            for (var i = 0; i < response.ProgramInformation.Pinnings.Length; i++)
            {
                var pinning = response.ProgramInformation.Pinnings[i];
                stringBuilder.AppendLine($"---{i + 1} of {response.ProgramInformation.Pinnings.Length}: ");
                stringBuilder.AppendLine($"------ID: {pinning.PinningID}");
                stringBuilder.AppendLine($"------Date: {pinning.DateTime.Day}/{pinning.DateTime.Month}/{pinning.DateTime.Year}");
                stringBuilder.AppendLine($"------Time: {pinning.DateTime.Hour}:{pinning.DateTime.Minute}:{pinning.DateTime.Second}");

                for (var j = 0; j < pinning.Positions.Length; j++)
                {
                    var position = pinning.Positions[j];
                    stringBuilder.AppendLine($"---------Position {j + 1} of {pinning.Positions.Length}:");
                    stringBuilder.AppendLine($"------------Plate ID: {position.PlateID}");
                    stringBuilder.AppendLine($"------------Plate Name: {position.PlateName}");
                    stringBuilder.AppendLine($"------------Location: {position.Location}");
                    stringBuilder.AppendLine($"------------X: {position.X}");
                    stringBuilder.AppendLine($"------------Y: {position.Y}");
                    stringBuilder.AppendLine($"------------Result: {position.Result}");
                }
            }

            if (response.ProgramInformation.Pinnings.Length == 0)
                stringBuilder.AppendLine("-None");

            LogInfo(stringBuilder.ToString());
            AppendLineBreak(LineBreakCharacter);
        }

        /// <summary>
        /// Handle a Run Colony Detection command response.
        /// </summary>
        /// <param name="response">The command response.</param>
        private static void OnRunColonyDetectionWorkflowResponseCallback(RunColonyDetectionCommandResponse response)
        {
            // format and display the output from the Colony Detection workflow on the console

            AppendLineBreak(LineBreakCharacter);

            // display program info
            LogInfo(response.Result.ToString());
            LogInfo($"Tracking base: {response.TrackingPath}");
            LogInfo($"Program ID: {response.ProgramInformation.ProgramID}");

            AppendLineBreak(LineBreakCharacter);
            LogInfo("Colonies:");

            var stringBuilder = new StringBuilder();

            // iterate and display all colony information
            for (var i = 0; i < response.ProgramInformation.ColonyInformation.Length; i++)
            {
                var colony = response.ProgramInformation.ColonyInformation[i];
                stringBuilder.AppendLine($"---{i + 1} of {response.ProgramInformation.ColonyInformation.Length}:");
                stringBuilder.AppendLine($"------ID: {colony.ID}");
                stringBuilder.AppendLine($"------Name: {colony.Name}");
                stringBuilder.AppendLine($"------IsSelected: {colony.IsSelected}");
                stringBuilder.AppendLine($"------SectorID: {colony.SectorID}");
                stringBuilder.AppendLine($"------X: {colony.X}");
                stringBuilder.AppendLine($"------Y: {colony.Y}");
                stringBuilder.AppendLine($"------Area: {colony.Area}");
                stringBuilder.AppendLine($"------Diameter: {colony.Diameter}");
                stringBuilder.AppendLine($"------Brightness: {colony.Brightness}");
                stringBuilder.AppendLine($"------AverageRed: {colony.AverageRed}");
                stringBuilder.AppendLine($"------AverageGreen: {colony.AverageGreen}");
                stringBuilder.AppendLine($"------AverageBlue: {colony.AverageBlue}");
                stringBuilder.AppendLine($"------Redness: {colony.Redness}");
                stringBuilder.AppendLine($"------Greenness: {colony.Greenness}");
                stringBuilder.AppendLine($"------Blueness: {colony.Blueness}");
                stringBuilder.AppendLine($"------ProximityToClosest: {colony.ProximityToClosest}");
                stringBuilder.AppendLine($"------Circularity: {colony.Circularity}");
            }

            if (response.ProgramInformation.ColonyInformation.Length == 0)
                stringBuilder.AppendLine("---None");

            LogInfo(stringBuilder.ToString());
            AppendLineBreak(LineBreakCharacter);
        }

        #endregion

        #region Main

        /// <summary>
        /// Main entry point for the application.
        /// </summary>
        /// <param name="args">
        /// Start up arguments. Valid values are:
        /// -P[HOST]:[PORT] - specify the PIXL's IP address and port number. For example -P127.0.0.1:50052 for an IP address of 127.0.0.1 and a port number of 50052.
        /// -D              - specify that debug information should be displayed on the Console. 
        /// </param>
        static void Main(string[] args)
        {
            try
            {
                // display startup
                DisplayStartup();

                // the gRPC channel
                Channel channel;

                // determine if using a specified host and port
                var useSpecifiedHostAndPort = args.Any(x => x.ToUpper().StartsWith(SpecifyHostAndPortArgument));

                // determine if debug info should be displayed
                var displayDebugInfo = args.Any(x => x.ToUpper().StartsWith(DisplayDebugInformationArgument));

                // if using a specified host and port
                if (useSpecifiedHostAndPort)
                {
                    // get the host and port argument
                    var hostAndPortArgument = args.FirstOrDefault(x => x.ToUpper().StartsWith(SpecifyHostAndPortArgument)) ?? string.Empty;

                    // try and parse host and port
                    if (!TryParseHostAndPort(hostAndPortArgument, out var host, out var port))
                        throw new ArgumentException($"{hostAndPortArgument} could not be parsed to a host and port.");

                    // display connection method
                    LogInfo($"Connecting to the PIXL with specified socket ({host}:{port}).");

                    // use socket to find the PIXL
                    channel = PIXLClient.GetPIXLChannel(host, port);
                }
                else
                {
                    // display connection method
                    LogInfo("Connecting to the PIXL using SiLA discovery.");

                    // use SiLA Server Discovery to find the PIXL
                    channel = PIXLClient.GetPIXLChannel();
                }

                // if no channel was created there is an issue with either connectivity or the host and port (if specified)
                if (channel == null)
                    throw new NullReferenceException("No PIXL found.");

                // create client
                PIXL = new PIXLClient(channel);

                // display SiLA2 device info on the Console
                DisplaySiLA2DeviceInfo();

                // subscribe to events
                SubscribeToEvents();

                // set if debugging info should be displayed
                DisplayDebuggingInformation = displayDebugInfo;

                // process all input
                ProcessInput();

                // line break
                AppendLineBreak(LineBreakCharacter);
            }
            catch (Exception e)
            {
                // log the error
                LogError(ErrorHandling.HandleException(e));
            }
            finally
            {
                // dispose the PIXL
                PIXL?.Dispose();
            }

            // display message
            LogInfo("Press any key to exit...");

            // wait for a key press
            Console.ReadKey();
        }

        #endregion

        #region HelperMethods

        /// <summary>
        /// Subscribe to events from a PIXL client.
        /// </summary>
        private static void SubscribeToEvents()
        {
            // handle required plate interaction instructions changed
            PIXL.RequiredPlateInteractionInstructionsChanged += (s, e) => LogInfo(e);

            // handle required plate interactions changed
            PIXL.RequiredPlateInteractionsChanged += (s, e) => PendingPlateRequests = e;

            // handle active error information changed
            PIXL.ActiveErrorChanged += (s, e) =>
            {
                // check for error and display
                if (e.HasActiveError)
                    LogError($"Error: {e.Code}: {e.Description}");
            };

            // handle connection changed
            PIXL.IsConnectionLiveChanged += (s, e) => LogInfo($"The connection to the PIXL has changed and is now {(e ? "live" : "dead")}.");
        }

        /// <summary>
        /// Display the startup message on the Console.
        /// </summary>
        private static void DisplayStartup()
        {
            // display startup
            AppendLineBreak(LineBreakCharacter);
            LogInfo("Starting SI.PIXL.Client.Console.exe.");
            AppendLineBreak(LineBreakCharacter);
            LogInfo("This program can be started with the following arguments:");
            LogInfo("   -P[HOST]:[PORT] : Specify the PIXL's IP address and port number.");
            LogInfo("                     For example -P127.0.0.1:50052 for an IP address of 127.0.0.1 and a port number of 50052.");
            LogInfo("   -D              : Specify that debug information should be displayed on the Console.");
            AppendLineBreak(LineBreakCharacter);
        }

        /// <summary>
        /// Display SiLA2 device info for the PIXL.
        /// </summary>
        private static void DisplaySiLA2DeviceInfo()
        {
            // read properties from SiLAService feature
            Console.WriteLine("SiLA server description: " + PIXL.ServerDescription);
            Console.WriteLine("SilA server vendor URL: " + PIXL.ServerVendorUrl);
            Console.WriteLine("SiLA server version: " + PIXL.ServerVersion);
        }

        /// <summary>
        /// Display info about the PIXL.
        /// </summary>
        private static void DisplayInfo()
        {
            LogInfo("DEVICE INFO:");
            LogInfo($"{ChildElementPrefex}Serial number:                                          {PIXL.SerialNumber}");
            LogInfo($"{ChildElementPrefex}Software Version:                                       {PIXL.SoftwareVersion}");
            LogInfo($"{ChildElementPrefex}Server Description:                                     {PIXL.ServerDescription}");
            LogInfo($"{ChildElementPrefex}Server Vendor Url:                                      {PIXL.ServerVendorUrl}");
            LogInfo($"{ChildElementPrefex}Server Version:                                         {PIXL.ServerVersion}");
            LogInfo($"{ChildElementPrefex}Is Door Closed:                                         {PIXL.IsDoorClosed}");
            LogInfo($"{ChildElementPrefex}Is Door Locked:                                         {PIXL.IsDoorLocked}");
            LogInfo("STATUS INFO");
            LogInfo($"{ChildElementPrefex}Command Name:                                           {PIXL.OperationalStatus.CommandName}");
            LogInfo($"{ChildElementPrefex}Command Status:                                         {PIXL.OperationalStatus.CommandStatus}");
            LogInfo($"{ChildElementPrefex}Command Percentage Complete:                            {Math.Round(PIXL.OperationalStatus.ProgressAsPercentage, 1)}%");
            LogInfo($"{ChildElementPrefex}Command Estimated Remaining Time:                       {PIXL.OperationalStatus.EstimatedRemainingTime}");
            LogInfo("INITIALISED INFO:");
            LogInfo($"{ChildElementPrefex}Is Initialised:                                         {PIXL.IsInitialised}");
            LogInfo("COMSUMABLES INFO:");
            LogInfo($"{ChildElementPrefex}UV Bulb Total Time:                                     {PIXL.UVBulbTotalDuration}");
            LogInfo($"{ChildElementPrefex}UV Bulb Remaining Time:                                 {PIXL.UVBulbRemainingDuration}");
            LogInfo($"{ChildElementPrefex}Cleaves on current blade:                               {PIXL.CleavesOnCurrentBlade}");
            LogInfo($"{ChildElementPrefex}Cleaves remaining on current blade:                     {PIXL.RemainingCleavesOnCurrentBlade}");
            LogInfo($"{ChildElementPrefex}PickupLine used (m):                                    {PIXL.PickupLineUsedInMeters}");
            LogInfo($"{ChildElementPrefex}PickupLine remaining (m):                               {PIXL.PickupLineRemainingInMeters}");
            LogInfo($"{ChildElementPrefex}Allow workflow execution with insufficient PickupLine:  {PIXL.IsWorkflowExecutionAllowedWithInsufficientPickupLine}");
            LogInfo("POWER INFO:");
            LogInfo($"{ChildElementPrefex}Is Asleep:                                              {PIXL.IsAsleep}");
            LogInfo($"{ChildElementPrefex}Can Sleep:                                              {PIXL.CanSleep}");
            LogInfo($"{ChildElementPrefex}Can Awaken:                                             {PIXL.CanAwaken}");
        }

        /// <summary>
        /// Display the commands list.
        /// </summary>
        private static void DisplayCommandsList()
        {
            LogInfo("ABORT              Abort any running operation");
            LogInfo("AWAKEN             Awaken the PIXL");
            LogInfo("BAYSTATUS          Display the status of each bay");
            LogInfo("DETERMINEPOSITION  Determine the next available position given a project template and a row and column [TEMPLATE] [ROW] [COLUMN]");
            LogInfo("EXIT               Exit the client");
            LogInfo("HELP               View command list");
            LogInfo("INFO               Display information about the PIXL");
            LogInfo("INITIALISE         Initialise the PIXL");
            LogInfo("LOADED             Notify the PIXL that a plate has been loaded in the format [BAY] [ID] [TYPE] [ROLE]");
            LogInfo("LISTPLATES         Lists the available plates and their available plate dimension file names");
            LogInfo("LISTPROFILES       Lists the available pinning profiles");
            LogInfo("LISTTEMPLATES      Lists the available project templates");
            LogInfo("MOTORISEDDOORCLOSE     Close the motorised door");
            LogInfo("MOTORISEDDOOROPEN      Open the motorised door");
            LogInfo("REMOVED            Notify the PIXL that a plate has been removed in the format [BAY]");
            LogInfo("REMOVEDALL         Notify the PIXL that all plates have been removed");
            LogInfo("RESET              Reset the PIXL");
            LogInfo("RESTART            Restart the PIXL");
            LogInfo("RUNCD              Run an example of the colony detection protocol");
            LogInfo("RUNRCP             Run an example of the random colony picking protocol");
            LogInfo("RUNREARRAY         Run an example of the re-array protocol");
            LogInfo("RUNREARRAYFILE     Run an example of the re-array protocol from a file a specified number of times [PATH] [ITERATIONS]");
            LogInfo("RUNUV              Run the UV sterilisation with a specified duration in [MINUTES]");
            LogInfo("SLEEP              Put the PIXL to sleep");
            LogInfo("SHUTDOWN           Shutdown the PIXL");
        }

        /// <summary>
        /// Display the available plates.
        /// </summary>
        private static void DisplayAvailablePlates()
        {
            // display header
            LogInfo("AVAILABLE PLATE TYPES:");

            // hold plate types
            var plateTypes = new[]
            {
                PlateTypes.PlusPlate,
                PlateTypes.PlusPlate_6,
                PlateTypes.PlusPlate_12,
                PlateTypes.PlusPlate_24,
                PlateTypes.PlusPlate_48,
                PlateTypes.PlusPlate_96,
                PlateTypes.PlusPlate_384,
                PlateTypes.PlusPlate_1536,
                PlateTypes.Petri_90,
                PlateTypes.Petri_150,
                PlateTypes.DeepMWP_6,
                PlateTypes.DeepMWP_12,
                PlateTypes.DeepMWP_24,
                PlateTypes.DeepMWP_48,
                PlateTypes.DeepMWP_96,
                PlateTypes.DeepMWP_384,
                PlateTypes.MWP_6,
                PlateTypes.MWP_12,
                PlateTypes.MWP_24,
                PlateTypes.MWP_48,
                PlateTypes.MWP_96,
                PlateTypes.MWP_384,
                PlateTypes.PCR_96_200ul,
                PlateTypes.PCR_96_300ul,
                PlateTypes.PCR_384_40ul
            };

            // iterate plate types
            foreach (var platetype in plateTypes)
            {
                // get all dimension names
                var plateDimensionNames = PIXL.GetAvailablePlateDimensionFileNames(platetype);

                // display type on console
                LogInfo($"{ChildElementPrefex}{platetype}");

                // display all available plate dimensions on console
                foreach (var profile in plateDimensionNames)
                    LogInfo($"{ChildElementPrefex}{ChildElementPrefex}{profile}");
            }
        }

        /// <summary>
        /// Display the available pinning profiles.
        /// </summary>
        private static void DisplayAvailablePinningProfiles()
        {
            // get all profiles
            var profiles = PIXL.AvailablePinningProfiles;

            // display header
            LogInfo("AVAILABLE PINNING PROFILES:");

            // display on console
            foreach (var profile in profiles)
                LogInfo($"{ChildElementPrefex}{profile}");
        }

        /// <summary>
        /// Display the available project templates.
        /// </summary>
        private static void DisplayAvailableProjectTemplates()
        {
            // get all random colony picking templates
            var templates = PIXL.AvailableRandomColonyPickingProjectTemplates;

            // display header
            LogInfo("AVAILABLE RANDOM COLONY PICKING PROJECT TEMPLATES:");

            // display on console
            foreach (var template in templates)
                LogInfo($"{ChildElementPrefex}{template}");

            // get all re-array templates
            templates = PIXL.AvailableRearrayProjectTemplates;

            // display header
            LogInfo("AVAILABLE RE-ARRAY PROJECT TEMPLATES:");

            // display on console
            foreach (var template in templates)
                LogInfo($"{ChildElementPrefex}{template}");

            // get all colony detection templates
            templates = PIXL.AvailableColonyDetectionProjectTemplates;

            // display header
            LogInfo("AVAILABLE COLONY DETECTION PROJECT TEMPLATES:");

            // display on console
            foreach (var template in templates)
                LogInfo($"{ChildElementPrefex}{template}");
        }

        #endregion

        #region ExceptionHandling

        /// <summary>
        /// Handle a gRPC exception.
        /// </summary>
        /// <param name="e">The exception to handle.</param>
        private static void HandleRPCException(RpcException e)
        {
            LogError(ErrorHandling.HandleException(e));
        }

        #endregion

        #region EventHandlers

        /// <summary>
        /// Handle PIXL debug information received.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The debugging information.</param>
        private static void PIXL_DebugInformationReceived(object sender, string e)
        {
            // log the debugging information
            LogDebugInfo(e);
        }

        #endregion
    }
}
See Also