///
/// Nanotec Nanolib example
/// Copyright (C) Nanotec GmbH & Co. KG - All Rights Reserved
///
/// This product includes software developed by the
/// Nanotec GmbH & Co. KG (http://www.nanotec.com/).
///
/// The Nanolib interface headers and the examples source code provided are 
/// licensed under the Creative Commons Attribution 4.0 Internaltional License. 
/// To view a copy of this license, 
/// visit https://creativecommons.org/licenses/by/4.0/ or send a letter to 
/// Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
///
/// The parts of the library provided in binary format are licensed under 
/// the Creative Commons Attribution-NoDerivatives 4.0 International License. 
/// To view a copy of this license, 
/// visit http://creativecommons.org/licenses/by-nd/4.0/ or send a letter to 
/// Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
///
/// This program is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
///
/// <file_name>MenuUtils.cs</file_name>
///
/// <summary>Definition of CLI menu specific classes</summary>
///
/// <date>29-10-2024</date>
///
/// <author>Michael Milbradt<author>
///

using NanolibExample;
using Nlc;
using System.Globalization;
using System.Text;

namespace MenuUtils
{
    /// <summary>
    /// Collection for used object dictionary indices
    /// </summary>
    public static class OdIndices
    {
        public static readonly OdIndex OdSIUnitPosition = new OdIndex(0x60A8, 0x00);
        public static readonly OdIndex OdControlWord = new OdIndex(0x6040, 0x00);
        public static readonly OdIndex OdStatusWord = new OdIndex(0x6041, 0x00);
        public static readonly OdIndex OdHomePage = new OdIndex(0x6505, 0x00);
        public static readonly OdIndex OdNanoJControl = new OdIndex(0x2300, 0x00);
        public static readonly OdIndex OdNanoJStatus = new OdIndex(0x2301, 0x00);
        public static readonly OdIndex OdNanoJError = new OdIndex(0x2302, 0x00);
        public static readonly OdIndex OdModeOfOperation = new OdIndex(0x6060, 0x00);
        public static readonly OdIndex OdTargetVelocity = new OdIndex(0x60FF, 0x00);
        public static readonly OdIndex OdProfileVelocity = new OdIndex(0x6081, 0x00);
        public static readonly OdIndex OdTargetPosition = new OdIndex(0x607A, 0x00);
        public static readonly UInt16 OdErrorStackIndex = 0x1003;
        public static readonly OdIndex OdErrorCount = new OdIndex(0x1003, 0x00);
        public static readonly OdIndex OdPosEncoderIncrementsInterface1 = new OdIndex(0x60E6, 0x1);
        public static readonly OdIndex OdPosEncoderIncrementsInterface2 = new OdIndex(0x60E6, 0x2);
        public static readonly OdIndex OdPosEncoderIncrementsInterface3 = new OdIndex(0x60E6, 0x3);
        public static readonly OdIndex OdMotorDriveSubmodeSelect = new OdIndex(0x3202, 0x00);
        public static readonly OdIndex OdStoreAllParams = new OdIndex(0x1010, 0x01);
        public static readonly OdIndex OdRestoreAllDefParams = new OdIndex(0x1011, 0x01);
        public static readonly OdIndex OdRestoreTuningDefParams = new OdIndex(0x1011, 0x06);
        public static readonly OdIndex OdModeOfOperationDisplay = new OdIndex(0x6061, 0x00);
    }

    /// <summary>
    /// Collection of menu texts / menu titles
    /// </summary>
    public static class MenuTexts
    {
        public const string BUS_HARDWARE_MENU = "Bus Hardware Menu";
        public const string BUS_HARDWARE_OPEN_MI = "Open Bus Hardware";
        public const string BUS_HARDWARE_CLOSE_MI = "Close bus hardware";
        public const string BUS_HARDWARE_SCAN_MI = "Scan for Bus hardware";
        public const string BUS_HARDWARE_CLOSE_ALL_MI = "Close all bus hardware";

        public const string DEVICE_MENU = "Device Menu";
        public const string DEVICE_SCAN_MI = "Scan for Devices";
        public const string DEVICE_CONNECT_MENU = "Connect to device Menu";
        public const string DEVICE_DISCONNECT_MENU = "Disconnect from device Menu";
        public const string DEVICE_SELECT_ACTIVE_MENU = "Select active device";
        public const string DEVICE_REBOOT_MI = "Reboot device";
        public const string DEVICE_UPDATE_FW_MI = "Update firmware";
        public const string DEVICE_UPDATE_BL_MI = "Update bootloader";
        public const string DEVICE_UPLOAD_NANOJ_MI = "Upload NanoJ program";
        public const string DEVICE_RUN_NANOJ_MI = "Run NanoJ program";
        public const string DEVICE_STOP_NANOJ_MI = "Stop NanoJ program";

        public const string DEVICE_INFORMATION_MENU = "Device information Menu";
        public const string DEVICE_GET_VENDOR_ID_MI = "Read vendor Id";
        public const string DEVICE_GET_PRODUCT_CODE_MI = "Read product code";
        public const string DEVICE_GET_DEVICE_NAME_MI = "Read device name";
        public const string DEVICE_GET_HW_VERSION_MI = "Read device hardware version";
        public const string DEVICE_GET_FW_BUILD_ID_MI = "Read device firmware build id";
        public const string DEVICE_GET_BL_BUILD_ID_MI = "Read device bootloader build id";
        public const string DEVICE_GET_SERIAL_NUMBER_MI = "Read device serial number";
        public const string DEVICE_GET_UNIQUE_ID_MI = "Read device unique id";
        public const string DEVICE_GET_BL_VERSION_MI = "Read device bootloader version";
        public const string DEVICE_GET_HW_GROUP_MI = "Read device hardware group";
        public const string DEVICE_GET_CON_STATE_MI = "Read device connection state";
        public const string DEVICE_GET_ERROR_FIELD_MI = "Read device error field";
        public const string DEVICE_RESTORE_ALL_DEFAULT_PARAMS_MI = "Restore all default parameters";

        public const string OD_INTERFACE_MENU = "Object Dictionary Interface Menu";
        public const string OD_ASSIGN_OD_MI = "Assign an object dictionary to active device (e.g. od.xml)";
        public const string OD_READ_NUMBER_MI = "readNumber (raw, untyped)";
        public const string OD_READ_STRING_MI = "readString";
        public const string OD_READ_BYTES_MI = "readBytes (raw, untyped)";
        public const string OD_WRITE_NUMBER_MI = "writeNumber (data bitlength needed)";
        public const string OD_READ_NUMBER_VIA_OD_MI = "readNumber (via OD interface, get type information)";
        public const string OD_WRITE_NUMBER_VIA_OD_MI = "writeNumber (via OD interface, no data bitlength needed)";

        public const string LOGGING_MENU = "Logging Menu";
        public const string LOGGING_SET_LOG_LEVEL_MI = "Set log level";
        public const string LOGGING_SET_LOG_CALLBACK_MI = "Set logging callback";

        public const string LOG_LEVEL_MENU = "Log level Menu";
        public const string LOG_LEVEL_TRACE_MI = "Set log level to 'Trace'";
        public const string LOG_LEVEL_DEBUG_MI = "Set log level to 'Debug'";
        public const string LOG_LEVEL_INFO_MI = "Set log level to 'Info'";
        public const string LOG_LEVEL_WARN_MI = "Set log level to 'Warning'";
        public const string LOG_LEVEL_ERROR_MI = "Set log level to 'Error'";
        public const string LOG_LEVEL_CRITICAL_MI = "Set log level to 'Critical'";
        public const string LOG_LEVEL_OFF_MI = "Set log level to 'Off'";

        public const string LOG_CALLBACK_MENU = "Logging Callback Menu";
        public const string LOG_CALLBACK_CORE_MI = "Activate log callback for Nanolib Core";
        public const string LOG_CALLBACK_CANOPEN_MI = "Activate log callback for CANopen module";
        public const string LOG_CALLBACK_ETHERCAT_MI = "Activate log callback for EtherCAT module";
        public const string LOG_CALLBACK_MODBUS_MI = "Activate log callback for Modbus module";
        public const string LOG_CALLBACK_REST_MI = "Activate log callback for REST module";
        public const string LOG_CALLBACK_USB_MI = "Activate log callback for USB/MSC module";
        public const string LOG_CALLBACK_DEACTIVATE_MI = "Deactivate current log callback";

        public const string SAMPLER_EXAMPLE_MENU = "Sampler Example Menu";
        public const string SAMPLER_NORMAL_WO_NOTIFY_MI = "Sampler w/o Notification - Normal Mode";
        public const string SAMPLER_REPETETIVE_WO_NOTIFY_MI = "Sampler w/o Notification - Repetetive Mode";
        public const string SAMPLER_CONTINUOUS_WO_NOTIFY_MI = "Sampler w/o Notification - Continuous Mode";
        public const string SAMPLER_NORMAL_WITH_NOTIFY_MI = "Sampler with Notification - Normal Mode";
        public const string SAMPLER_REPETETIVE_WITH_NOTIFY_MI = "Sampler with Notification - Repetetive Mode";
        public const string SAMPLER_CONTINUOUS_WITH_NOTIFY_MI = "Sampler with Notification - Continuous Mode";

        public const string MOTOR_EXAMPLE_MENU = "Motor Example Menu";
        public const string MOTOR_AUTO_SETUP_MI = "Initial commissioning - motor auto setup";
        public const string MOTOR_VELOCITY_MI = "Run a motor in profile velocity mode";
        public const string MOTOR_POSITIONING_MI = "Run a motor in positioning mode";

        public const string PROFINET_EXAMPLE_MI = "ProfinetDCP example";

        public const string MAIN_MENU = "Nanolib Example Main";
    }

    /// <summary>
    /// Context class, used to store context and menu specific variables
    /// </summary>
    public class Context
    {
        public Context()
        {
            this.SelectedOption = 0;
            this.ErrorText = "";
            this.CurrentLogLevel = LogLevel.Off;
            this.NanolibAccessor = Nanolib.getNanoLibAccessor();
            this.ScannedBusHardwareIds = new BusHWIdVector();
            this.OpenableBusHardwareIds = new BusHWIdVector();
            this.OpenBusHardwareIds = new BusHWIdVector();
            this.ScannedDeviceIds = new List<DeviceId>();
            this.ConnectableDeviceIds = new List<DeviceId>();
            this.ConnectedDeviceHandles = new List<DeviceHandle>();
            this.ActiveDevice = new DeviceHandle();
            this.CurrentLogModule = new LogModule();
            this.WaitForUserConfirmation = false;
            this.LoggingCallbackActive = false;
            this.LoggingCallback = new LoggingCallbackExample(); // Instantiate a logging callback 
            this.ScanBusCallback = new ScanBusCallbackExample(); // Instantiate a scan bus callback 
            this.DataTransferCallback = new DataTransferCallbackExample(); // Instantiate a data transfer callback 
            this.Red = new ColorModifier(Code.FG_RED);
            this.Green = new ColorModifier(Code.FG_GREEN);
            this.Blue = new ColorModifier(Code.FG_BLUE);
            this.Yellow = new ColorModifier(Code.FG_YELLOW);
            this.LightRed = new ColorModifier(Code.FG_LIGHT_RED);
            this.LightGreen = new ColorModifier(Code.FG_LIGHT_GREEN);
            this.LightBlue = new ColorModifier(Code.FG_LIGHT_BLUE);
            this.LightYellow = new ColorModifier(Code.FG_LIGHT_YELLOW);
            this.DarkGray = new ColorModifier(Code.FG_DARK_GRAY);
            this.Def = new ColorModifier(Code.FG_DEFAULT);
            this.ResetAll = new ColorModifier(Code.RESET);
        }

        public int SelectedOption { get; set; }
        public string ErrorText { get; set; }
        public LogLevel CurrentLogLevel { get; set; }
        public NanoLibAccessor NanolibAccessor { get; set; }
        public BusHWIdVector ScannedBusHardwareIds { get; set; }
        public BusHWIdVector OpenableBusHardwareIds { get; set; }
        public BusHWIdVector OpenBusHardwareIds { get; set; }
        public List<DeviceId> ScannedDeviceIds { get; set; }
        public List<DeviceId> ConnectableDeviceIds { get; set; }
        public List<DeviceHandle> ConnectedDeviceHandles { get; set; }
        public DeviceHandle ActiveDevice { get; set; }
        public LogModule CurrentLogModule { get; set; }
        public bool LoggingCallbackActive { get; set; }
        public bool WaitForUserConfirmation { get; set; }
        public LoggingCallbackExample LoggingCallback { get; set; }
        public ScanBusCallbackExample ScanBusCallback { get; set; }
        public DataTransferCallbackExample DataTransferCallback { get; set; }
        public ColorModifier Red { get; set; }
        public ColorModifier Green { get; set; }
        public ColorModifier Blue { get; set; }
        public ColorModifier Yellow { get; set; }
        public ColorModifier LightRed { get; set; }
        public ColorModifier LightGreen { get; set; }
        public ColorModifier LightBlue { get; set; }
        public ColorModifier LightYellow { get; set; }
        public ColorModifier DarkGray { get; set; }
        public ColorModifier Def { get; set; }
        public ColorModifier ResetAll { get; set; }
    }

    /// <summary>
    /// Helper class to decode error code, error number and error class
    /// </summary>
    public static class ErrorHelper
    {
        public static string GetErrorNumberString(long number)
        {
            uint bitMask = 0xff000000;
            byte byteValue = (byte)((number & bitMask) >> 24);
            string resultString;

            switch (byteValue)
            {
                case 0:
                    resultString = "    0: Watchdog Reset";
                    break;
                case 1:
                    resultString = "    1: Input voltage (+Ub) too high";
                    break;
                case 2:
                    resultString = "    2: Output current too high";
                    break;
                case 3:
                    resultString = "    3: Input voltage (+Ub) too low";
                    break;
                case 4:
                    resultString = "    4: Error at fieldbus";
                    break;
                case 6:
                    resultString = "    6: CANopen only: NMT master takes too long to send Nodeguarding request";
                    break;
                case 7:
                    resultString = "    7: Sensor 1 (see 3204h): Error through electrical fault or defective hardware";
                    break;
                case 8:
                    resultString = "    8: Sensor 2 (see 3204h): Error through electrical fault or defective hardware";
                    break;
                case 9:
                    resultString = "    9: Sensor 3 (see 3204h): Error through electrical fault or defective hardware";
                    break;
                case 10:
                    resultString = "   10: Positive limit switch exceeded";
                    break;
                case 11:
                    resultString = "   11: Negative limit switch exceeded";
                    break;
                case 12:
                    resultString = "   12: Overtemperature error";
                    break;
                case 13:
                    resultString = "   13: The values of object 6065h and 6066h were exceeded; a fault was triggered.";
                    break;
                case 14:
                    resultString = "   14: Nonvolatile memory full. Controller must be restarted for cleanup work.";
                    break;
                case 15:
                    resultString = "   15: Motor blocked";
                    break;
                case 16:
                    resultString = "   16: Nonvolatile memory damaged; controller must be restarted for cleanup work.";
                    break;
                case 17:
                    resultString = "   17: CANopen only: Slave took too long to send PDO messages.";
                    break;
                case 18:
                    resultString = "   18: Sensor n (see 3204h), where n is greater than 3: Error through electrical fault or defective hardware";
                    break;
                case 19:
                    resultString = "   19: CANopen only: PDO not processed due to a length error.";
                    break;
                case 20:
                    resultString = "   20: CANopen only: PDO length exceeded.";
                    break;
                case 21:
                    resultString = "   21: Restart the controller to avoid future errors when saving (nonvolatile memory full/corrupt).";
                    break;
                case 22:
                    resultString = "   22: Rated current must be set (203Bh:01h/6075h).";
                    break;
                case 23:
                    resultString = "   23: Encoder resolution, number of pole pairs and some other values are incorrect.";
                    break;
                case 24:
                    resultString = "   24: Motor current is too high, adjust the PI parameters.";
                    break;
                case 25:
                    resultString = "   25: Internal software error, generic.";
                    break;
                case 26:
                    resultString = "   26: Current too high at digital output.";
                    break;
                case 27:
                    resultString = "   27: CANopen only: Unexpected sync length.";
                    break;
                case 30:
                    resultString = "   30: Error in speed monitoring: slippage error too large.";
                    break;
                case 32:
                    resultString = "   32: Internal error: Correction factor for reference voltage missing in the OTP.";
                    break;
                case 35:
                    resultString = "   35: STO Fault: STO was requested but not via both STO inputs";
                    break;
                case 36:
                    resultString = "   36: STO Changeover: STO was requested but not via both STO inputs.";
                    break;
                case 37:
                    resultString = "   37: STO Active: STO is active, it generates no torque or holding torque.";
                    break;
                case 38:
                    resultString = "   38: STO Self-Test: Error during self-test of the firmware. Contact Nanotec.";
                    break;
                case 39:
                    resultString = "   39: Error in the ballast configuration: Invalid/unrealistic parameters entered.";
                    break;
                case 40:
                    resultString = "   40: Ballast resistor thermally overloaded.";
                    break;
                case 41:
                    resultString = "   41: Only EtherCAT: Sync Manager Watchdog: The controller has not received any PDO data for an excessively long period of time.";
                    break;
                case 46:
                    resultString = "   46: Interlock error: Bit 3 in 60FDh is set to 0, the motor may not start.";
                    break;
                case 48:
                    resultString = "   48: Only CANopen: NMT status has been set to stopped.";
                    break;
                default:
                    resultString = "   " + byteValue + ": Unknown error number";
                    break;
            }

            return resultString;
        }

        public static string GetErrorClassString(long number)
        {
            uint bitMask = 0xff0000;
            byte byteValue = (byte)((number & bitMask) >> 16);
            string resultString;

            switch (byteValue)
            {
                case 1:
                    resultString = "    1: General error, always set in the event of an error.";
                    break;
                case 2:
                    resultString = "    2: Current.";
                    break;
                case 4:
                    resultString = "    4: Voltage.";
                    break;
                case 8:
                    resultString = "    8: Temperature.";
                    break;
                case 16:
                    resultString = "   16: Communication";
                    break;
                case 32:
                    resultString = "   32: Relates to the device profile.";
                    break;
                case 64:
                    resultString = "   64: Reserved, always 0.";
                    break;
                case 128:
                    resultString = "  128: Manufacturer-specific.";
                    break;
                default:
                    resultString = "  " + byteValue + ": Unkonw error class.";
                    break;
            }

            return resultString;
        }

        public static string GetErrorCodeString(long number)
        {
            uint bitMask = 0xffff;
            ushort wordValue = (ushort)(number & bitMask);
            string resultString;

            switch (wordValue)
            {
                case 0x1000:
                    resultString = "0x1000: General error.";
                    break;
                case 0x2300:
                    resultString = "0x2300: Current at the controller output too large.";
                    break;
                case 0x3100:
                    resultString = "0x3100: Overvoltage/undervoltage at controller input.";
                    break;
                case 0x4200:
                    resultString = "0x4200: Temperature error within the controller.";
                    break;
                case 0x5440:
                    resultString = "0x5440: Interlock error: Bit 3 in 60FDh is set to 0, the motor may not start .";
                    break;
                case 0x6010:
                    resultString = "0x6010: Software reset (watchdog).";
                    break;
                case 0x6100:
                    resultString = "0x6100: Internal software error, generic.";
                    break;
                case 0x6320:
                    resultString = "0x6320: Rated current must be set (203Bh:01h/6075h).";
                    break;
                case 0x7110:
                    resultString = "0x7110: Error in the ballast configuration: Invalid/unrealistic parameters entered.";
                    break;
                case 0x7113:
                    resultString = "0x7113: Warning: Ballast resistor thermally overloaded.";
                    break;
                case 0x7121:
                    resultString = "0x7121: Motor blocked.";
                    break;
                case 0x7200:
                    resultString = "0x7200: Internal error: Correction factor for reference voltage missing in the OTP.";
                    break;
                case 0x7305:
                    resultString = "0x7305: Sensor 1 (see 3204h) faulty.";
                    break;
                case 0x7306:
                    resultString = "0x7306: Sensor 2 (see 3204h) faulty.";
                    break;
                case 0x7307:
                    resultString = "0x7307: Sensor n (see 3204h), where n is greater than 2.";
                    break;
                case 0x7600:
                    resultString = "0x7600: Warning: Nonvolatile memory full or corrupt; restart the controller for cleanup work.";
                    break;
                case 0x8100:
                    resultString = "0x8100: Error during fieldbus monitoring.";
                    break;
                case 0x8130:
                    resultString = "0x8130: CANopen only: Life Guard error or Heartbeat error.";
                    break;
                case 0x8200:
                    resultString = "0x8200: CANopen only: Slave took too long to send PDO messages.";
                    break;
                case 0x8210:
                    resultString = "0x8210: CANopen only: PDO was not processed due to a length error.";
                    break;
                case 0x8220:
                    resultString = "0x8220: CANopen only: PDO length exceeded.";
                    break;
                case 0x8240:
                    resultString = "0x8240: CANopen only: unexpected sync length.";
                    break;
                case 0x8400:
                    resultString = "0x8400: Error in speed monitoring: slippage error too large.";
                    break;
                case 0x8611:
                    resultString = "0x8611: Position monitoring error: Following error too large.";
                    break;
                case 0x8612:
                    resultString = "0x8612: Position monitoring error: Limit switch exceeded.";
                    break;
                default:
                    resultString = wordValue + ": Uknown error code.";
                    break;
            }

            return resultString;
        }
    }

    /// <summary>
    /// Helper class to create bus hardware options
    /// </summary>
    public static class BusHardwareOptionsHelper
    {
        /// <summary> 
        /// Helper function to generate the proper bus hardware options
        /// </summary>
        /// <param name="busHardwareId">BusHardwareId</param>
        /// <returns>returns BusHardwareOptions</returns>
        public static BusHardwareOptions CreateBusHardwareOptions(BusHardwareId busHardwareId)
        {
            BusHardwareOptions busHwOptions = new BusHardwareOptions();

            // now add all options necessary for opening the bus hardware
            // in case of CAN bus it is the baud rate
            BusHwOptionsDefault busHwOptionsDefaults = new BusHwOptionsDefault();

            if (busHardwareId.getProtocol() == Nanolib.BUS_HARDWARE_ID_PROTOCOL_CANOPEN)
            {
                busHwOptions.addOption(busHwOptionsDefaults.canBus.BAUD_RATE_OPTIONS_NAME,
                                       busHwOptionsDefaults.canBus.baudRate.BAUD_RATE_1000K);

                if (busHardwareId.getBusHardware() == Nanolib.BUS_HARDWARE_ID_IXXAT)
                {
                    busHwOptions.addOption(
                        busHwOptionsDefaults.canBus.ixxat.ADAPTER_BUS_NUMBER_OPTIONS_NAME,
                        busHwOptionsDefaults.canBus.ixxat.adapterBusNumber.BUS_NUMBER_0_DEFAULT);
                }

                if (busHardwareId.getBusHardware() == Nanolib.BUS_HARDWARE_ID_PEAK)
                {
                    busHwOptions.addOption(
                        busHwOptionsDefaults.canBus.peak.ADAPTER_BUS_NUMBER_OPTIONS_NAME,
                        busHwOptionsDefaults.canBus.peak.adapterBusNumber.BUS_NUMBER_1_DEFAULT);
                }
            }
            else if (busHardwareId.getProtocol() == Nanolib.BUS_HARDWARE_ID_PROTOCOL_MODBUS_RTU)
            {
                busHwOptions.addOption(busHwOptionsDefaults.serial.BAUD_RATE_OPTIONS_NAME,
                                       busHwOptionsDefaults.serial.baudRate.BAUD_RATE_19200);
                busHwOptions.addOption(busHwOptionsDefaults.serial.PARITY_OPTIONS_NAME,
                                       busHwOptionsDefaults.serial.parity.EVEN);
            }

            return busHwOptions;
        }
    }

    /// <summary>
    /// Helper class for console input
    /// </summary>
    public static class StringUtils
    {
        /// <summary>
        /// Checks if string starts with a number
        /// </summary>
        /// <param name="s">string to check</param>
        /// <returns>returns true or false</returns>
        public static bool StartsWithDigit(string s)
        {
            if (string.IsNullOrEmpty(s))
                return false;

            if (char.IsDigit(s[0]))
                return true;

            return (s[0] == '-' || s[0] == '+') && s.Length > 1 && char.IsDigit(s[1]);
        }

        /// <summary>
        /// Converts a string to a number
        /// </summary>
        /// <param name="st">string to convert</param>
        /// <returns>returns either value of converted number or null if text number cannot be converted</returns>
        public static int? Stonum(string st)
        {
            var trimmed = st.Trim();
            bool ok = StartsWithDigit(trimmed);

            if (ok && int.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v))
            {
                return v;
            }

            return null;
        }

        /// <summary>
        /// Obtain a line of text from specified stream.
        /// </summary>
        /// <param name="reader">input stream</param>
        /// <param name="def">optional default text if no text entered</param>
        /// <returns>either valid input line or null if problem obtaining input</returns>
        public static string GetLine(TextReader reader, string def = "")
        {
            string ln = reader.ReadLine();
            return string.IsNullOrWhiteSpace(ln) && !string.IsNullOrEmpty(def) ? def : ln;
        }

        /// <summary>
        /// Obtain a line of text from console.
        /// </summary>
        /// <param name="prompt">optional prompt text to display first</param>
        /// <param name="def">optional default text</param>
        /// <returns>returns valid input line</returns>
        public static string GetLine(string prompt = "", string def = "")
        {
            string o;
            do
            {
                Console.Write(prompt);
                if (!string.IsNullOrEmpty(def))
                    Console.Write($" [{def}]");

                Console.Write(": ");
                o = GetLine(Console.In, def);
                if (string.IsNullOrEmpty(o))
                    Console.WriteLine("Invalid input");
            } while (string.IsNullOrEmpty(o));

            return o;
        }

        /// <summary>
        /// Extract next item of data from specified stream.
        /// </summary>
        /// <typeparam name="T">Default type is string</typeparam>
        /// <param name="reader">stream from which to extract data</param>
        /// <returns>either valid extracted data or null if problem extracting data</returns>
        public static T GetData<T>(TextReader reader)
        {
            string input = reader.ReadLine();
            if (!string.IsNullOrWhiteSpace(input) && char.IsWhiteSpace(input.Last()))
            {
                return (T)Convert.ChangeType(input.Trim(), typeof(T));
            }
            return default;
        }

        /// <summary>
        /// Obtains a number from specified stream.
        /// </summary>
        /// <typeparam name="T">Default number type is int</typeparam>
        /// <param name="reader">stream from which to obtain number</param>
        /// <param name="wholeLine">true if only one number per line (default), false if can have multiple numbers per line</param>
        /// <returns>either valid number of required type or null if problem extracting data</returns>
        public static int? GetNum<T>(TextReader reader, bool wholeLine = true) where T : struct
        {
            if (wholeLine)
            {
                string line = GetLine(reader);
                return line != null ? Stonum(line) : (int?)null;
            }

            return (int?)Convert.ChangeType(GetData<string>(reader), typeof(T));
        }

        /// <summary>
        /// Obtains a number from the console.
        /// </summary>
        /// <typeparam name="T">Default number type is int</typeparam>
        /// <param name="prompt">optional prompt text to display first</param>
        /// <param name="nmin">optional minimum valid value</param>
        /// <param name="nmax">optional maximum valid value</param>
        /// <param name="wholeLine">true if only one number per line (default), false if can have multiple numbers per line</param>
        /// <returns>returns when valid number entered</returns>
        public static int GetNum<T>(string prompt = "", T nmin = default, T nmax = default, bool wholeLine = true) where T : IComparable
        {
            Console.Write(prompt);
            Console.Write($" ({nmin} - {nmax}): ");
            
            int? o = GetNum<int>(Console.In, wholeLine);
            
            if (!o.HasValue || (o < (int)(object)nmin || o > (int)(object)nmax))
                o = int.MaxValue;

            return o.Value;
        }

        /// <summary>
        /// Obtains a char from the specified stream.
        /// </summary>
        /// <param name="reader">stream from which to obtain number</param>
        /// <param name="def">default char to return if no character obtained</param>
        /// <param name="wholeLine">true if only one char per line (default), false if can have multiple chars per line</param>
        /// <returns>returns either valid character or null if problem extracting data</returns>
        public static char? GetChar(TextReader reader, char def = '\0', bool wholeLine = true)
        {
            if (wholeLine)
            {
                string line = GetLine(reader);
                return string.IsNullOrEmpty(line) ? def : (char?)line.FirstOrDefault();
            }
            return (char?)GetData<string>(reader).FirstOrDefault();
        }

        /// <summary>
        /// Obtains a char from the console.
        /// </summary>
        /// <param name="prompt">optional prompt text to display first</param>
        /// <param name="valid">optional string containing valid values for the char.</param>
        /// <param name="def">optional default char to use if none entered.</param>
        /// <param name="wholeLine">true if only one char per line (default), false if can have multiple chars per line</param>
        /// <returns>returns valid char.</returns>
        public static char GetChar(string prompt = "", string valid = "", char def = '\0', bool wholeLine = true)
        {
            char? o;
            do
            {
                Console.Write(prompt);
                if (!string.IsNullOrEmpty(valid))
                    Console.Write($" ({string.Join("/", valid)})");

                Console.Write(": ");
                o = GetChar(Console.In, def, wholeLine);
                if (!o.HasValue || (!string.IsNullOrEmpty(valid) && !valid.Contains(o.Value)))
                    Console.WriteLine("Invalid input");
            } while (!o.HasValue || (!string.IsNullOrEmpty(valid) && !valid.Contains(o.Value)));

            return o.Value;
        }

        /// <summary>
        /// Displays an error message.
        /// </summary>
        /// <param name="ctx">Menu context.</param>
        /// <param name="errorString">General error or warning string, will be displayed in yellow.</param>
        /// <param name="errorReasonString">Abort error from application or Nanolib, will be displayed in red.</param>
        /// <returns>Returns the generated string.</returns>
        public static string HandleErrorMessage(Context ctx, string errorString, string errorReasonString = "")
        {
            var errorMessage = new System.Text.StringBuilder();
            // Color error string in light yellow and reason in light red
            errorMessage.Append(ctx.LightYellow);
            errorMessage.Append(errorString);
            errorMessage.Append(ctx.LightRed);
            errorMessage.Append(errorReasonString);
            errorMessage.Append(ctx.Def);

            // For menu info
            ctx.ErrorText = errorMessage.ToString();

            // For current output, if waitForUserConfirmation is true
            // otherwise we can skip the output
            if (ctx.WaitForUserConfirmation)
            {
                Console.WriteLine(errorMessage.ToString());
            }

            return errorMessage.ToString();
        }

    }

    /// <summary>
    /// Delegate for submenu or function calls
    /// </summary>
    public delegate void FunctionPointer(Context ctx);

    /// <summary>
    /// Menu class for CLI Menu
    /// </summary>
    public class Menu
    {
        /// <summary>
        /// MenuItem contains a name and a pointer to a menu or a function
        /// </summary>
        public class MenuItem
        {
            public MenuItem(string Name, object Func, bool IsActive)
            {
                this.Name = Name;
                this.Func = Func;
                this.IsActive = IsActive;
            }

            public string Name;
            public object Func; // Can hold either FunctionPointer or Menu
            public bool IsActive;
        }

        /// <summary>
        /// Using directive for list containing menu items
        /// </summary>
        private List<MenuItem> menuItems;

        private string title;
        private object? defaultFunc;

        /// <summary>
        /// Default constructor
        /// </summary>
        public Menu()
        {
            this.title = string.Empty;
            this.defaultFunc = null;
            this.menuItems = new List<MenuItem>();
        }

        /// <summary>
        /// Menu constructor with all params
        /// </summary>
        /// <param name="title">the menu title</param>
        /// <param name="menuItems">the menu items to show</param>
        /// <param name="defaultFunc">pointer to default function to use for dynamic menu</param>
        public Menu(string title, List<MenuItem> menuItems, object defaultFunc)
        {
            this.title = title;
            this.menuItems = menuItems;
            this.defaultFunc = defaultFunc;
        }

        /// <summary>
        /// Menu constructor with all params
        /// </summary>
        /// <param name="title">the menu title</param>
        /// <param name="menuItems">the menu items to show</param>
        /// <param name="defaultFunc">pointer to default function to use for dynamic menu</param>
        public Menu(string title, List<MenuItem> menuItems)
        {
            this.title = title;
            this.menuItems = menuItems;
            this.defaultFunc = null;
        }

        /// <summary>
        /// Gets the title of a menu
        /// </summary>
        /// <returns>returns the title as string</returns>
        public string GetTitle() => title;

        /// <summary>
        /// Set a title of a menu
        /// </summary>
        /// <param name="t">the title to use as string</param>
        public void SetTitle(string t)
        {
            title = t;
        }

        /// <summary>
        /// Get the configured default function for dynamic menu
        /// </summary>
        /// <returns>returns a pointer to a function or null</returns>
        public object GetDefaultFunction() => defaultFunc;

        /// <summary>
        /// Delete a menu item
        /// </summary>
        /// <param name="index">index of item in list to delete</param>
        /// <returns>returns true if found and deleted</returns>
        public bool EraseMenuItem(int index)
        {
            if (index >= 0 && index < menuItems.Count)
            {
                menuItems.RemoveAt(index);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Delete all configured menu items 
        /// </summary>
        /// <returns>returns true</returns>
        public bool EraseAllMenuItems()
        {
            menuItems.Clear();
            return true;
        }

        /// <summary>
        /// Add a menu item (no duplication check)
        /// </summary>
        /// <param name="menuItem">the menu item to add (append)</param>
        /// <returns>returns true</returns>
        public bool AppendMenuItem(MenuItem menuItem)
        {
            menuItems.Add(menuItem);
            return true;
        }

        /// <summary>
        /// Inserts a menu item at index
        /// </summary>
        /// <param name="index">the position to insert the menu item</param>
        /// <param name="menuItem">the menu item to insert</param>
        /// <returns>returns true if insert was successful</returns>
        public bool InsertMenuItem(int index, MenuItem menuItem)
        {
            if (index >= 0 && index <= menuItems.Count)
            {
                menuItems.Insert(index, menuItem);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Prints basic information for the user
        /// </summary>
        /// <param name="ctx">menu context</param>
        /// <returns>the complete string for output</returns>
        public string PrintInfo(Context ctx)
        {
            // Clear screen, return value not needed
            Console.Clear();
            var oss = new StringBuilder();

            oss.Append(GetActiveDeviceString(ctx));
            oss.Append(GetFoundBusHwString(ctx));
            oss.Append(GetOpenedBusHwIdString(ctx));
            oss.Append(GetScannedDeviceIdsString(ctx));
            oss.Append(GetConnectedDevicesString(ctx));
            oss.Append(GetCallbackLoggingString(ctx));
            oss.Append(GetObjectDictionaryString(ctx));
            oss.AppendLine($"Log level        : {LogLevelConverter.toString(ctx.CurrentLogLevel)}");
            oss.AppendLine(ctx.ErrorText);
            ctx.ErrorText = ""; // Clear text

            return oss.ToString();
        }

        /// <summary>
        /// Helper function for comparing DeviceIds
        /// </summary>
        /// <param name="deviceId">First device id</param>
        /// <param name="otherDeviceId">Second device id</param>
        /// <returns>true if equals, false otherwise</returns>
        public static bool IsSameDeviceId(DeviceId deviceId, DeviceId otherDeviceId)
        {
            // compare BH and device id parts by hand
            // since there is no working equal inteface generated
            if ((deviceId.getBusHardwareId().getBusHardware() == otherDeviceId.getBusHardwareId().getBusHardware()) &&
                        (deviceId.getBusHardwareId().getHardwareSpecifier() == otherDeviceId.getBusHardwareId().getHardwareSpecifier()) &&
                        (deviceId.getBusHardwareId().getExtraHardwareSpecifier() == otherDeviceId.getBusHardwareId().getExtraHardwareSpecifier()) &&
                        (deviceId.getBusHardwareId().getName() == otherDeviceId.getBusHardwareId().getName()) &&
                        (deviceId.getBusHardwareId().getProtocol() == otherDeviceId.getBusHardwareId().getProtocol()) &&
                        (deviceId.getDeviceId() == otherDeviceId.getDeviceId()) &&
                        (deviceId.getExtraId().SequenceEqual(otherDeviceId.getExtraId())) &&
                        (deviceId.getExtraStringId() == otherDeviceId.getExtraStringId()))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Get all found devices not yet connected
        /// </summary>
        /// <param name="ctx">The menu context</param>
        /// <returns>returns a list of DeviceId for all found devices not yet connected</returns>
        public static List<DeviceId> GetConnectableDeviceIds(Context ctx)
        {
            var result = new List<DeviceId>();

            foreach (var scannedDeviceId in ctx.ScannedDeviceIds)
            {
                bool alreadyConnected = false;

                foreach (var connectedDeviceHandle in ctx.ConnectedDeviceHandles)
                {
                    var connectedDeviceId = ctx.NanolibAccessor.getDeviceId(connectedDeviceHandle).getResult();

                    if (IsSameDeviceId(connectedDeviceId, scannedDeviceId))
                    {
                        alreadyConnected = true;
                        break;
                    }
                }

                if (!alreadyConnected)
                {
                    result.Add(scannedDeviceId);
                }
            }

            return result;
        }

        /// <summary>
        /// Get all found bus harware ids not yet opened
        /// </summary>
        /// <param name="ctx">The menu context</param>
        /// <returns>returns a vector of BusHardwareId for all found bus hardware ids not yet opened</returns>
        public static BusHWIdVector GetOpenableBusHwIds(Context ctx)
        {
            var result = new BusHWIdVector();

            foreach (var scannedBusHw in ctx.ScannedBusHardwareIds)
            {
                bool alreadyOpened = false;

                foreach (var openBusHwId in ctx.OpenBusHardwareIds)
                {
                    if ((openBusHwId.getBusHardware() == scannedBusHw.getBusHardware()) && 
                        (openBusHwId.getHardwareSpecifier() == scannedBusHw.getHardwareSpecifier()) && 
                        (openBusHwId.getExtraHardwareSpecifier() == scannedBusHw.getExtraHardwareSpecifier()) &&
                        (openBusHwId.getName() == scannedBusHw.getName()) &&
                        (openBusHwId.getProtocol() == scannedBusHw.getProtocol()))
                    {
                        alreadyOpened = true;
                        break;
                    }
                }

                if (!alreadyOpened)
                {
                    result.Add(scannedBusHw);
                }
            }

            return result;
        }

        /// <summary>
        /// Sets the default menu items with given name and default function for dynamic menus
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <param name="ctx">The menu context</param>
        public static void SetMenuItems(Menu menu, Context ctx)
        {
            if (menu.GetTitle() == MenuTexts.MAIN_MENU)
            {
                foreach (var mi in menu.menuItems)
                {
                    if (mi.Name == MenuTexts.BUS_HARDWARE_MENU)
                    {
                        mi.IsActive = true;
                    }
                    else if (mi.Name == MenuTexts.DEVICE_MENU)
                    {
                        mi.IsActive = ctx.OpenBusHardwareIds.Count > 0;
                    }
                    else if (new[] { MenuTexts.OD_INTERFACE_MENU, 
                                    MenuTexts.SAMPLER_EXAMPLE_MENU, 
                        MenuTexts.MOTOR_EXAMPLE_MENU, 
                        MenuTexts.PROFINET_EXAMPLE_MI }.Contains(mi.Name))
                    {
                        mi.IsActive = !ctx.ActiveDevice.Equals(new DeviceHandle());
                    }
                    else if (mi.Name == MenuTexts.LOGGING_MENU)
                    {
                        mi.IsActive = true;
                    }
                }
            }
            else if (menu.GetTitle() == MenuTexts.BUS_HARDWARE_MENU)
            {
                foreach (var mi in menu.menuItems)
                {
                    if (mi.Name == MenuTexts.BUS_HARDWARE_SCAN_MI)
                    {
                        mi.IsActive = true;
                    }
                    else if (mi.Name == MenuTexts.BUS_HARDWARE_OPEN_MI) 
                    {
                        mi.IsActive = ctx.OpenableBusHardwareIds.Count > 0;
                    }
                    else if (new[] { MenuTexts.BUS_HARDWARE_CLOSE_MI, 
                        MenuTexts.BUS_HARDWARE_CLOSE_ALL_MI }.Contains(mi.Name))
                    {
                        mi.IsActive = ctx.OpenBusHardwareIds.Count > 0;
                    }
                }
            }
            else if (menu.GetTitle() == MenuTexts.DEVICE_MENU)
            {
                foreach (var mi in menu.menuItems)
                {
                    if (mi.Name == MenuTexts.DEVICE_SCAN_MI)
                    {
                        mi.IsActive = ctx.OpenBusHardwareIds.Count > 0;
                    }
                    else if (mi.Name == MenuTexts.DEVICE_CONNECT_MENU)
                    {
                        mi.IsActive = (ctx.ConnectableDeviceIds.Count > 0) && (ctx.OpenBusHardwareIds.Count > 0) ;
                    }
                    else if (new[] { MenuTexts.DEVICE_DISCONNECT_MENU, 
                        MenuTexts.DEVICE_SELECT_ACTIVE_MENU }.Contains(mi.Name))
                    {
                        mi.IsActive = ctx.ConnectedDeviceHandles.Count > 0;
                    }
                    else if (new[] { MenuTexts.DEVICE_INFORMATION_MENU, 
                        MenuTexts.DEVICE_REBOOT_MI, 
                        MenuTexts.DEVICE_UPDATE_FW_MI, 
                        MenuTexts.DEVICE_UPDATE_BL_MI, 
                        MenuTexts.DEVICE_UPLOAD_NANOJ_MI, 
                        MenuTexts.DEVICE_RUN_NANOJ_MI, 
                        MenuTexts.DEVICE_STOP_NANOJ_MI, 
                        MenuTexts.DEVICE_GET_ERROR_FIELD_MI, 
                        MenuTexts.DEVICE_RESTORE_ALL_DEFAULT_PARAMS_MI }.Contains(mi.Name))
                    {
                        mi.IsActive = !ctx.ActiveDevice.Equals(new DeviceHandle());
                    }
                }
            }
            else if (new[] { MenuTexts.DEVICE_INFORMATION_MENU, 
                MenuTexts.OD_INTERFACE_MENU, 
                MenuTexts.SAMPLER_EXAMPLE_MENU, 
                MenuTexts.MOTOR_EXAMPLE_MENU }.Contains(menu.GetTitle()))
            {
                foreach (var mi in menu.menuItems)
                {
                    mi.IsActive = !ctx.ActiveDevice.Equals(new DeviceHandle());
                }
            }
            else if (new[] { MenuTexts.LOG_LEVEL_MENU, 
                MenuTexts.LOGGING_MENU, 
                MenuTexts.LOG_CALLBACK_MENU }.Contains(menu.GetTitle()))
            {
                foreach (var mi in menu.menuItems)
                {
                    mi.IsActive = true;
                }
            }
            else if (menu.GetTitle() == MenuTexts.BUS_HARDWARE_OPEN_MI)
            {
                menu.EraseAllMenuItems();
                var openableBusHardwareIds = GetOpenableBusHwIds(ctx);

                foreach (var openableBusHwId in openableBusHardwareIds)
                {
                    var mi = new MenuItem($"{openableBusHwId.getProtocol()} ({openableBusHwId.getName()})",
                        menu.GetDefaultFunction(),
                        true);
                    menu.AppendMenuItem(mi);
                }
            }
            else if (menu.GetTitle() == MenuTexts.BUS_HARDWARE_CLOSE_MI)
            {
                menu.EraseAllMenuItems();
                var openBusHwIds = ctx.OpenBusHardwareIds;

                foreach (var openBusHwId in openBusHwIds)
                {
                    var mi = new MenuItem($"{openBusHwId.getProtocol()} ({openBusHwId.getBusHardware()})",
                        menu.GetDefaultFunction(),
                        true);
                    menu.AppendMenuItem(mi);
                }
            }
            else if (menu.GetTitle() == MenuTexts.DEVICE_CONNECT_MENU)
            {
                menu.EraseAllMenuItems();
                var connectableDeviceIds = GetConnectableDeviceIds(ctx);

                foreach (var connectableDeviceId in connectableDeviceIds)
                {
                    var mi = new MenuItem($"{connectableDeviceId.getDescription()} [id: {connectableDeviceId.getDeviceId()}, protocol: {connectableDeviceId.getBusHardwareId().getProtocol()}, hw: {connectableDeviceId.getBusHardwareId().getName()}]",
                        menu.GetDefaultFunction(),
                        true);
                    menu.AppendMenuItem(mi);
                }
            }
            else if (menu.GetTitle() == MenuTexts.DEVICE_DISCONNECT_MENU)
            {
                menu.EraseAllMenuItems();
                var openDeviceIds = new List<DeviceId>();

                foreach (var openDeviceHandle in ctx.ConnectedDeviceHandles)
                {
                    var openDeviceIdResult = ctx.NanolibAccessor.getDeviceId(openDeviceHandle);
                    if (openDeviceIdResult.hasError())
                    {
                        continue;
                    }
                    openDeviceIds.Add(openDeviceIdResult.getResult());
                }

                foreach (var deviceId in openDeviceIds)
                {
                    var mi = new MenuItem($"{deviceId.getDescription()} [id: {deviceId.getDeviceId()}, protocol: {deviceId.getBusHardwareId().getProtocol()}, hw: {deviceId.getBusHardwareId().getName()}]",
                        menu.GetDefaultFunction(),
                        true);
                    menu.AppendMenuItem(mi);
                }
            }
            else if (menu.GetTitle() == MenuTexts.DEVICE_SELECT_ACTIVE_MENU)
            {
                menu.EraseAllMenuItems();
                var connectedDeviceHandles = ctx.ConnectedDeviceHandles;

                foreach (var connectedDeviceHandle in connectedDeviceHandles)
                {
                    var deviceIdResult = ctx.NanolibAccessor.getDeviceId(connectedDeviceHandle);
                    if (deviceIdResult.hasError())
                    {
                        continue;
                    }
                    var deviceId = deviceIdResult.getResult();
                    var mi = new MenuItem($"{deviceId.getDescription()} [id: {deviceId.getDeviceId()}, protocol: {deviceId.getBusHardwareId().getProtocol()}, hw: {deviceId.getBusHardwareId().getName()}]",
                        menu.GetDefaultFunction(),
                        true);
                    menu.AppendMenuItem(mi);
                }
            }
        }
    
        /// <summary>
        /// Build the active device string for printInfo
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <returns>returns the resulting string</returns>
        public string GetActiveDeviceString(Context ctx)
        {
            var result = new StringBuilder();
            result.AppendLine($"Active device    : {ctx.DarkGray}None{ctx.Def}");

            if (ctx.ActiveDevice.Equals(new DeviceHandle()))
            {
                return result.ToString();
            }

            var activeDevice = ctx.NanolibAccessor.getDeviceId(ctx.ActiveDevice).getResult();
            result.Clear();
            result.AppendLine($"Active device    : {ctx.LightGreen}{activeDevice.getDescription()} [id: {activeDevice.getDeviceId()}, protocol: {activeDevice.getBusHardwareId().getProtocol()}, hw: {activeDevice.getBusHardwareId().getName()}]{ctx.Def}");
            
            return result.ToString();
        }

        /// <summary>
        /// Build the number of found bus hardware for printInfo
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <returns>returns the resulting string</returns>
        public string GetFoundBusHwString(Context ctx)
        {
            var result = new StringBuilder();
            result.AppendLine($"Bus HW found     : {ctx.DarkGray}None (not scanned?){ctx.Def}");

            if (!ctx.ScannedBusHardwareIds.Any())
            {
                return result.ToString();
            }

            result.Clear();
            result.AppendLine($"Bus HW found     : {ctx.LightGreen}{ctx.ScannedBusHardwareIds.Count}{ctx.Def}");
            return result.ToString();
        }

        /// <summary>
        /// Build the opened bus hardware string for printInfo
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <returns>returns the resulting string</returns>
        public string GetOpenedBusHwIdString(Context ctx)
        {
            var result = new StringBuilder();
            result.AppendLine($"Open Bus HW      : {ctx.DarkGray}None{ctx.Def}");

            if (!ctx.OpenBusHardwareIds.Any())
            {
                return result.ToString();
            }

            result.Clear();
            result.Append("Open Bus HW      : ");
            bool firstItem = true;

            foreach (var openBusHardwareId in ctx.OpenBusHardwareIds)
            {
                if (firstItem)
                {
                    result.Append($"{ctx.LightGreen}{openBusHardwareId.getProtocol()} ({openBusHardwareId.getName()}){ctx.Def}");
                    firstItem = false;
                }
                else
                {
                    result.Append($", {ctx.LightGreen}{openBusHardwareId.getProtocol()} ({openBusHardwareId.getName()}){ctx.Def}");
                }
            }
            result.AppendLine();

            return result.ToString();
        }

        /// <summary>
        /// Build the number of found devices printInfo
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <returns>returns the resulting string</returns>
        public string GetScannedDeviceIdsString(Context ctx)
        {
            var result = new StringBuilder();
            result.AppendLine($"Device(s) found  : {ctx.DarkGray}None (not scanned?){ctx.Def}");

            if (!ctx.ScannedDeviceIds.Any())
            {
                return result.ToString();
            }

            result.Clear();
            result.AppendLine($"Device(s) found  : {ctx.LightGreen}{ctx.ScannedDeviceIds.Count}{ctx.Def}");
            return result.ToString();
        }

        /// <summary>
        /// Build the connected device(s) string for printInfo
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <returns>returns the resulting string</returns>
        public string GetConnectedDevicesString(Context ctx)
        {
            var result = new StringBuilder();
            result.AppendLine($"Connected devices: {ctx.DarkGray}None{ctx.Def}");

            if (!ctx.ConnectedDeviceHandles.Any())
            {
                return result.ToString();
            }

            result.Clear();
            result.Append("Connected devices: ");
            bool firstItem = true;

            foreach (var connectedDeviceHandle in ctx.ConnectedDeviceHandles)
            {
                var resultDeviceId = ctx.NanolibAccessor.getDeviceId(connectedDeviceHandle);
                if (resultDeviceId.hasError())
                {
                    continue;
                }
                var connectedDeviceId = resultDeviceId.getResult();

                if (firstItem)
                {
                    result.Append($"{ctx.LightGreen}{connectedDeviceId.getDescription()} [id: {connectedDeviceId.getDeviceId()}, protocol: {connectedDeviceId.getBusHardwareId().getProtocol()}, hw: {connectedDeviceId.getBusHardwareId().getName()}]{ctx.Def}");
                    firstItem = false;
                }
                else
                {
                    result.Append($", {ctx.LightGreen}{connectedDeviceId.getDescription()} [id: {connectedDeviceId.getDeviceId()}, protocol: {connectedDeviceId.getBusHardwareId().getProtocol()}, hw: {connectedDeviceId.getBusHardwareId().getName()}]{ctx.Def}");
                }
            }

            result.AppendLine();
            return result.ToString();
        }

        /// <summary>
        /// Build the callback logging string for printInfo
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <returns>returns the resulting string</returns>
        public string GetCallbackLoggingString(Context ctx)
        {
            var result = new StringBuilder();
            result.AppendLine("Callback Logging : Off");

            if (!ctx.LoggingCallbackActive)
            {
                return result.ToString();
            }

            result.Clear();
            result.AppendLine($"Callback Logging : {ctx.LightGreen}On{ctx.Def} ({LogModuleConverter.toString(ctx.CurrentLogModule)})");
            return result.ToString();
        }

        /// <summary>
        /// Build the object dictionary string for printInfo
        /// </summary>
        /// <param name="menu">The Menu to use</param>
        /// <returns>returns the resulting string</returns>
        public string GetObjectDictionaryString(Context ctx)
        {
            var result = new StringBuilder();
            result.AppendLine($"Object dictionary: {ctx.DarkGray}Fallback (not assigned){ctx.Def}");

            if (ctx.ActiveDevice.Equals(new DeviceHandle()))
            {
                return result.ToString();
            }

            var resultObjectDictionary = ctx.NanolibAccessor.getAssignedObjectDictionary(ctx.ActiveDevice);
            if (resultObjectDictionary.hasError())
            {
                return result.ToString();
            }

            var objectDictionary = resultObjectDictionary.getResult();

            if (string.IsNullOrEmpty(objectDictionary.getXmlFileName().getResult()))
            {
                return result.ToString();
            }

            result.Clear();
            result.AppendLine($"Object dictionary: {ctx.LightGreen}Assigned{ctx.Def}");
            
            return result.ToString();
        }

        /// <summary>
        /// Display the menu, wait and get user input
        /// </summary>
        /// <param name="currentMenu">the menu to display</param>
        /// <param name="ctx">menu context</param>
        /// <returns>returns the user selected option</returns>
        public int ShowMenu(Menu currentMenu, Context ctx)
        {
            // Dynamic part (for some menus)
            SetMenuItems(currentMenu, ctx);
            // Static part
            var oss = new StringBuilder();
            var numberOfMenuItems = currentMenu.menuItems.Count;

            if (ctx.WaitForUserConfirmation)
            {
                Console.WriteLine("Press enter to continue!");
                Console.ReadLine();
            }
            ctx.WaitForUserConfirmation = false;

            // Create the user information part
            oss.Append(currentMenu.PrintInfo(ctx));
            // Create the menu header
            oss.AppendLine("---------------------------------------------------------------------------");
            oss.AppendLine($" {currentMenu.GetTitle()}");
            oss.AppendLine("---------------------------------------------------------------------------");

            // Create the menu items (options)
            for (int i = 1; i <= numberOfMenuItems; ++i)
            {
                if (currentMenu.menuItems[i - 1].IsActive)
                {
                    oss.AppendLine($"{ctx.Def}{((numberOfMenuItems > 9 && i < 10) ? " " : "")}{i}) {currentMenu.menuItems[i - 1].Name}");
                }
                else
                {
                    oss.AppendLine($"{ctx.DarkGray}{((numberOfMenuItems > 9 && i < 10) ? " " : "")}{i}) {currentMenu.menuItems[i - 1].Name}{ctx.Def}");
                }
            }

            // Create back (sub-menu) or exit option (main menu)
            if (currentMenu.GetTitle() == MenuTexts.MAIN_MENU)
            {
                oss.AppendLine($"\n{((numberOfMenuItems > 9) ? " " : "")}0) Exit program\n\nEnter menu option number");
            }
            else
            {
                oss.AppendLine($"\n{((numberOfMenuItems > 9) ? " " : "")}0) Back\n\nEnter menu option number");
            }

            // Display created output to screen and wait for user input
            return StringUtils.GetNum<int>(oss.ToString(), 0, numberOfMenuItems);
        }

        /// <summary>
        /// Menu loop, check selection and call function or sub menu entry
        /// </summary>
        /// <param name="currentMenu">the menu to display</param>
        /// <param name="ctx">menu context</param>
        public void MenuLoop(Menu menu, Context ctx)
        {
            // Clear screen, result not needed
            Console.Clear();

            ctx.WaitForUserConfirmation = false;
            for (int opt; (opt = ShowMenu(menu, ctx)) > 0;)
            {
                if ((opt == int.MaxValue) || !menu.menuItems[opt - 1].IsActive)
                {
                    var oss = new StringBuilder();
                    oss.Append($"{ctx.LightYellow}Invalid option{ctx.Def}");
                    ctx.ErrorText = oss.ToString();
                }
                else
                {
                    ctx.ErrorText = "";

                    // Store selected option to context
                    ctx.SelectedOption = opt;

                    var mi = menu.menuItems[opt - 1];
                    if (mi.Func is Delegate)
                    {
                        ((Action<Context>)(mi.Func))(ctx);
                    }
                    else
                    {
                        MenuLoop((Menu)mi.Func, ctx);
                    }
                }
            }
        }
    }
}



