private bool ScanPortWithIDCode(ushort comPort, ushort idCode, ScanParameters scanParams, CancellationToken cancellationToken)
        {
            string scanConnectionMode = scanParams.AutoStartParsingSequenceForScan ? ActiveScanConnectionString : PassiveScanConnectionString;
            string connectionString   = string.Format(ConnectionStringTemplate, comPort, Settings.BaudRate, Settings.Parity, Settings.StopBits, Settings.DataBits, Settings.DtrEnable, Settings.RtsEnable, scanConnectionMode);

            ShowUpdateMessage($"{Tab1}Scanning COM{comPort} with ID code {idCode}...");

            IConfigurationFrame configFrame = RequestDeviceConfiguration(connectionString, idCode, scanParams, cancellationToken);

            return(configFrame is not ConfigurationErrorFrame && SaveDeviceConfiguration(configFrame, comPort, idCode, scanParams));
        }
        private void ExecuteScan(ScanParameters scanParams, CancellationToken cancellationToken)
        {
            // Create wait handles to use to wait for configuration frame
            m_configurationWaitHandle ??= new ManualResetEventSlim(false);
            m_bytesReceivedWaitHandle ??= new ManualResetEventSlim(false);

            // Create a new phasor protocol frame parser used to dynamically request device configuration frames
            // and return them to remote clients so that the frame can be used in system setup and configuration
            if (m_frameParser is null)
            {
                m_frameParser = new MultiProtocolFrameParser();

                // Attach to events on new frame parser reference
                m_frameParser.ConnectionAttempt                 += m_frameParser_ConnectionAttempt;
                m_frameParser.ConnectionEstablished             += m_frameParser_ConnectionEstablished;
                m_frameParser.ConnectionException               += m_frameParser_ConnectionException;
                m_frameParser.ConnectionTerminated              += m_frameParser_ConnectionTerminated;
                m_frameParser.ExceededParsingExceptionThreshold += m_frameParser_ExceededParsingExceptionThreshold;
                m_frameParser.ParsingException           += m_frameParser_ParsingException;
                m_frameParser.ReceivedConfigurationFrame += m_frameParser_ReceivedConfigurationFrame;
                m_frameParser.BufferParsed += m_frameParser_BufferParsed;

                // We only want to try to connect to device and retrieve configuration as quickly as possible
                m_frameParser.MaximumConnectionAttempts = 1;
                m_frameParser.SourceName = Name;
                m_frameParser.AutoRepeatCapturedPlayback   = false;
                m_frameParser.AutoStartDataParsingSequence = false;
                m_frameParser.SkipDisableRealTimeData      = true;
            }

            Task.Run(() =>
            {
                ClearFeedback();
                UpdateFeedback();

                Ticks scanStartTime = DateTime.UtcNow.Ticks;

                try
                {
                    SetControlEnabledState(buttonScan, false);

                    TableOperations <Device> deviceTable = scanParams.DeviceTable;
                    ushort[] comPorts  = scanParams.ComPorts;
                    ushort[] idCodes   = scanParams.IDCodes;
                    bool rescan        = scanParams.Rescan;
                    int scannedIDCodes = 0;
                    HashSet <ushort> configuredPorts   = new();
                    HashSet <ushort> configuredIDCodes = new();
                    HashSet <ushort> idCodeSettings    = new(idCodes);
                    int discoveredDevices = 0;
                    long totalComScanTime = 0L;
                    long totalComScans    = 0L;

                    ShowUpdateMessage("Reading existing configuration...");
                    Device[] devices = deviceTable.QueryRecordsWhere("IsConcentrator = 0").ToArray();

                    // If re-scanning all ports, we will not skip pre-configured ports and ID codes
                    if (rescan)
                    {
                        ShowUpdateMessage($"{Tab1}Discovered {devices.Length:N0} existing devices{Environment.NewLine}");
                    }
                    else
                    {
                        foreach (Device device in devices)
                        {
                            if (device is null)
                            {
                                continue;
                            }

                            Dictionary <string, string> settings = device.ConnectionString.ParseKeyValuePairs();

                            if (settings.TryGetValue("port", out string portVal) && ushort.TryParse(portVal.Substring(3), out ushort port))
                            {
                                configuredPorts.Add(port);
                            }

                            configuredIDCodes.Add((ushort)device.AccessID);
                        }

                        ShowUpdateMessage($"{Tab1}Discovered {devices.Length:N0} existing devices, {configuredPorts.Count:N0} configured COM ports and {configuredIDCodes.Count:N0} unique ID codes{Environment.NewLine}");
                    }

                    // Hold onto to device list, useful when saving configurations later (no need to re-query)
                    scanParams.Devices = devices;

                    // Only control progress bar for manual (non-import) scans
                    if (buttonImport.Enabled)
                    {
                        SetProgressBarMinMax(0, idCodes.Length);
                        UpdateProgressBar(0);
                    }

                    foreach (ushort idCode in idCodes)
                    {
                        cancellationToken.ThrowIfCancellationRequested();

                        if (configuredIDCodes.Contains(idCode))
                        {
                            ShowUpdateMessage($"Skipping scan for already configured ID code {idCode}...");
                        }
                        else
                        {
                            Ticks idScanStartTime = DateTime.UtcNow.Ticks;

                            try
                            {
                                ShowUpdateMessage($"Starting scan for ID code {idCode}...");
                                int processedComPorts = 0;
                                bool found            = false;

                                HashSet <ushort> unprocessedComPorts = new(comPorts);
                                unprocessedComPorts.ExceptWith(configuredPorts);
                                comPorts = unprocessedComPorts.ToArray();

                                foreach (ushort comPort in comPorts)
                                {
                                    cancellationToken.ThrowIfCancellationRequested();

                                    Ticks comScanStartTime = DateTime.UtcNow.Ticks;
                                    bool scanned           = true;

                                    try
                                    {
                                        if (configuredPorts.Contains(comPort))
                                        {
                                            ShowUpdateMessage($"Skipping scan for already configured COM{comPort}...");
                                            scanned = false;
                                            continue;
                                        }

                                        if (!ScanPortWithIDCode(comPort, idCode, scanParams, cancellationToken))
                                        {
                                            continue;
                                        }

                                        // Shorten COM port scan list as new devices are detected
                                        configuredPorts.Add(comPort);
                                        UpdateFeedback(null, ++discoveredDevices);
                                        found = true;
                                        break;
                                    }
                                    finally
                                    {
                                        Ticks comScanTime = DateTime.UtcNow.Ticks - comScanStartTime;
                                        ShowUpdateMessage($"{Environment.NewLine}>> Scan time for COM{comPort} for ID code {idCode}: {comScanTime.ToElapsedTimeString(3)}.{Environment.NewLine}");

                                        processedComPorts++;
                                        totalComScanTime += comScanTime.Value;

                                        if (scanned)
                                        {
                                            totalComScans++;

                                            long remainingTimeEstimate =
                                                (idCodes.Length - configuredIDCodes.Count - scannedIDCodes) * comPorts.Length;

                                            if (!found)
                                            {
                                                remainingTimeEstimate += comPorts.Length - processedComPorts;
                                            }

                                            remainingTimeEstimate *= (long)(totalComScanTime / (double)totalComScans);
                                            UpdateFeedback(new Ticks(remainingTimeEstimate).ToElapsedTimeString(0));
                                        }
                                    }
                                }

                                ShowUpdateMessage($"Completed scan for ID code {idCode}.");
                            }
                            finally
                            {
                                ShowUpdateMessage($"{Environment.NewLine}>>>> Scan time for ID code {idCode}: {(DateTime.UtcNow.Ticks - idScanStartTime).ToElapsedTimeString(3)}.{Environment.NewLine}");
                            }
                        }

                        scannedIDCodes++;

                        // Only control progress bar for manual (non-import) scans
                        if (buttonImport.Enabled)
                        {
                            UpdateProgressBar(scannedIDCodes);
                        }

                        // Serialize reduced ID code list
                        try
                        {
                            // In case app needs to restart do not rescan existing ID codes
                            if (Settings.AutoRemoveIDs && idCodeSettings.Remove(idCode))
                            {
                                Settings.IDCodes = idCodeSettings.ToArray();
                                Settings.Save();
                            }
                        }
                        catch (Exception ex)
                        {
                            m_log.Publish(MessageLevel.Error, "UpdateIDCodes", "Failed while reducing ID code list", exception: ex);
                        }
                    }

                    ShowUpdateMessage($"Completed scan for {scannedIDCodes:N0} ID codes over {comPorts.Length:N0} COM ports.{Environment.NewLine}");
                    UpdateFeedback("None -- Operation Complete");
                }
                catch (OperationCanceledException)
                {
                    ShowUpdateMessage($"{Environment.NewLine}Serial port scan cancelled.{Environment.NewLine}");
                    UpdateFeedback("None -- Operation Cancelled");
                }
                catch (Exception ex)
                {
                    ShowUpdateMessage($"{Environment.NewLine}ERROR: Failed during serial port scan: {ex.Message}");
                    UpdateFeedback("None -- Operation Failed");
                }
                finally
                {
                    ShowUpdateMessage($">>>>>> Total scan time: {(DateTime.UtcNow.Ticks - scanStartTime).ToElapsedTimeString(3)}.{Environment.NewLine}");
                    SetControlEnabledState(buttonScan, true);
                    m_scanExecutionComplete.Set();
                }
            },
                     cancellationToken);
        }
        public IConfigurationFrame RequestDeviceConfiguration(string connectionString, int idCode, ScanParameters scanParams, CancellationToken cancellationToken)
        {
            if (string.IsNullOrEmpty(connectionString))
            {
                ShowUpdateMessage($"{Tab2}ERROR: No connection string was specified, request for configuration canceled.");
                return(new ConfigurationErrorFrame());
            }

            try
            {
                int responseTimeout    = scanParams.ResponseTimeout;
                int configFrameTimeout = scanParams.ConfigFrameTimeout;

                // Most of the parameters in the connection string will be for the data source in the frame parser
                // so we provide all of them, other parameters will simply be ignored
                m_frameParser.ConnectionString = connectionString;

                // Provide access ID to frame parser as this may be necessary to make a phasor connection
                m_frameParser.DeviceID = (ushort)idCode;

                // Clear any existing configuration frame
                m_configurationFrame = null;

                // Set auto-start parsing sequence for scan state
                m_autoStartParsingSequenceForScan = scanParams.AutoStartParsingSequenceForScan;

                // Inform user of temporary loss of command access
                ShowUpdateMessage($"{Tab2}Requesting device configuration...");

                // Make sure the wait handles are not set
                m_configurationWaitHandle.Reset();
                m_bytesReceivedWaitHandle.Reset();

                // Start the frame parser - this will attempt connection
                m_frameParser.Start();

                // Wait for any bytes received within configured response timeout
                if (!m_bytesReceivedWaitHandle.Wait(responseTimeout, cancellationToken))
                {
                    ShowUpdateMessage($"{Tab2}Timed-out waiting for device response.");
                }
                else
                {
                    // Wait to receive the configuration frame
                    if (!m_configurationWaitHandle.Wait(configFrameTimeout, cancellationToken))
                    {
                        ShowUpdateMessage($"{Tab2}Timed-out waiting to receive remote device configuration.");
                    }
                }

                // Terminate connection to device
                m_frameParser.Stop();

                if (m_configurationFrame is null)
                {
                    m_configurationFrame = new ConfigurationErrorFrame();
                    ShowUpdateMessage($"{Tab2}Failed to retrieve remote device configuration.");
                }

                return(m_configurationFrame);
            }
            catch (Exception ex)
            {
                ShowUpdateMessage($"{Tab2}ERROR: Failed to request configuration due to exception: {ex.Message}");
            }

            return(new ConfigurationErrorFrame());
        }
        private void SavePhasorMeasurement(SignalType signalType, Device device, IPhasorDefinition phasorDefinition, char phase, int index, int baseKV, TableOperations <Measurement> measurementTable, ScanParameters scanParams)
        {
            string signalReference = $"{device.Acronym}-{signalType.Suffix}{index}";

            // Query existing measurement record for specified signal reference - function will create a new blank measurement record if one does not exist
            Measurement measurement = measurementTable.QueryMeasurement(signalReference);
            string      pointTag    = scanParams.CreatePhasorPointTag(device.Acronym, signalType.Acronym, phasorDefinition.Label, phase.ToString(), index, baseKV);

            measurement.DeviceID          = device.ID;
            measurement.PointTag          = pointTag;
            measurement.Description       = $"{device.Acronym} {phasorDefinition.Label} {signalType.Name}";
            measurement.PhasorSourceIndex = index;
            measurement.SignalReference   = signalReference;
            measurement.SignalTypeID      = signalType.ID;
            measurement.Internal          = true;
            measurement.Enabled           = true;

            measurementTable.AddNewOrUpdateMeasurement(measurement);
        }
        private void SaveDevicePhasors(IConfigurationCell cell, Device device, TableOperations <Measurement> measurementTable, ScanParameters scanParams)
        {
            bool phaseMatchExact(string phaseLabel, string[] phaseMatches) =>
            phaseMatches.Any(match => phaseLabel.Equals(match, StringComparison.Ordinal));

            bool phaseEndsWith(string phaseLabel, string[] phaseMatches, bool ignoreCase) =>
            phaseMatches.Any(match => phaseLabel.EndsWith(match, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));

            bool phaseStartsWith(string phaseLabel, string[] phaseMatches, bool ignoreCase) =>
            phaseMatches.Any(match => phaseLabel.StartsWith(match, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));

            bool phaseContains(string phaseLabel, string[] phaseMatches, bool ignoreCase) =>
            phaseMatches.Any(match => phaseLabel.IndexOf(match, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) > -1);

            bool phaseMatchHighConfidence(string phaseLabel, string[] containsMatches, string[] endsWithMatches)
            {
                if (phaseEndsWith(phaseLabel, containsMatches, true))
                {
                    return(true);
                }

                if (phaseStartsWith(phaseLabel, containsMatches, true))
                {
                    return(true);
                }

                foreach (string match in containsMatches.Concat(endsWithMatches))
                {
                    string[] variations = { $" {match}", $"_{match}", $"-{match}", $".{match}" };

                    if (phaseEndsWith(phaseLabel, variations, false))
                    {
                        return(true);
                    }
                }

                foreach (string match in containsMatches)
                {
                    string[] variations = { $" {match} ", $"_{match}_", $"-{match}-", $"-{match}_", $"_{match}-", $".{match}." };

                    if (phaseContains(phaseLabel, variations, false))
                    {
                        return(true);
                    }
                }

                return(false);
            }

            char guessPhase(char phase, string phasorLabel)
            {
                if (phaseMatchExact(phasorLabel, new[] { "V1PM", "I1PM" }) || phaseMatchHighConfidence(phasorLabel, new[] { "V1", "VP", "I1", "IP", "VSEQ1", "ISEQ1" }, new[] { "POS", "V1PM", "I1PM", "PS", "PSV", "PSI" }) || phaseEndsWith(phasorLabel, new[] { "+SV", "+SI", "+V", "+I" }, true))
                {
                    return('+');
                }

                if (phaseMatchExact(phasorLabel, new[] { "V0PM", "I0PM", "VZPM", "IZPM" }) || phaseMatchHighConfidence(phasorLabel, new[] { "V0", "I0", "VSEQ0", "ISEQ0" }, new[] { "ZERO", "ZPV", "ZPI", "VSPM", "V0PM", "I0PM", "VZPM", "IZPM", "ZS", "ZSV", "ZSI" }) || phaseEndsWith(phasorLabel, new[] { "0SV", "0SI" }, true))
                {
                    return('0');
                }

                if (phaseMatchExact(phasorLabel, new[] { "VAPM", "IAPM" }) || phaseMatchHighConfidence(phasorLabel, new[] { "VA", "IA" }, new[] { "APV", "API", "VAPM", "IAPM", "AV", "AI" }))
                {
                    return('A');
                }

                if (phaseMatchExact(phasorLabel, new[] { "VBPM", "IBPM" }) || phaseMatchHighConfidence(phasorLabel, new[] { "VB", "IB" }, new[] { "BPV", "BPI", "VBPM", "IBPM", "BV", "BI" }))
                {
                    return('B');
                }

                if (phaseMatchExact(phasorLabel, new[] { "VCPM", "ICPM" }) || phaseMatchHighConfidence(phasorLabel, new[] { "VC", "IC" }, new[] { "CPV", "CPI", "VCPM", "ICPM", "CV", "CI" }))
                {
                    return('C');
                }

                if (phaseMatchExact(phasorLabel, new[] { "VNPM", "INPM" }) || phaseMatchHighConfidence(phasorLabel, new[] { "VN", "IN" }, new[] { "NEUT", "NPV", "NPI", "VNPM", "INPM", "NV", "NI" }))
                {
                    return('N');
                }

                if (phaseMatchExact(phasorLabel, new[] { "V2PM", "I2PM" }) || phaseMatchHighConfidence(phasorLabel, new[] { "V2", "I2", "VSEQ2", "ISEQ2" }, new[] { "NEG", "-SV", "-SI", "V2PM", "I2PM", "NS", "NSV", "NSI" }))
                {
                    return('-');
                }

                return(phase);
            }

            int guessBaseKV(int baseKV, string phasorLabel, string deviceLabel)
            {
                // Check phasor label before device
                foreach (string voltageLevel in s_commonVoltageLevels)
                {
                    if (phasorLabel.IndexOf(voltageLevel, StringComparison.Ordinal) > -1)
                    {
                        return(int.Parse(voltageLevel));
                    }
                }

                foreach (string voltageLevel in s_commonVoltageLevels)
                {
                    if (deviceLabel.IndexOf(voltageLevel, StringComparison.Ordinal) > -1)
                    {
                        return(int.Parse(voltageLevel));
                    }
                }

                return(baseKV);
            }

            AdoDataConnection        connection  = scanParams.Connection;
            TableOperations <Phasor> phasorTable = new(connection);

            // Get phasor signal types
            SignalType iphmSignalType = m_phasorSignalTypes["IPHM"];
            SignalType iphaSignalType = m_phasorSignalTypes["IPHA"];
            SignalType vphmSignalType = m_phasorSignalTypes["VPHM"];
            SignalType vphaSignalType = m_phasorSignalTypes["VPHA"];

            Phasor[] phasors = phasorTable.QueryPhasorsForDevice(device.ID).ToArray();

            bool dropAndAdd = phasors.Length != cell.PhasorDefinitions.Count;

            if (!dropAndAdd)
            {
                // Also do add operation if phasor source index records are not sequential
                if (phasors.Where((phasor, index) => phasor.SourceIndex != index + 1).Any())
                {
                    dropAndAdd = true;
                }
            }

            if (dropAndAdd)
            {
                if (cell.PhasorDefinitions.Count > 0)
                {
                    connection.DeletePhasorsForDevice(device.ID);
                }

                foreach (IPhasorDefinition phasorDefinition in cell.PhasorDefinitions)
                {
                    bool isVoltage = phasorDefinition.PhasorType == PhasorType.Voltage;

                    Phasor phasor = phasorTable.NewPhasor();
                    phasor.DeviceID            = device.ID;
                    phasor.Label               = phasorDefinition.Label;
                    phasor.Type                = isVoltage ? 'V' : 'I';
                    phasor.Phase               = guessPhase('+', phasor.Label);
                    phasor.BaseKV              = guessBaseKV(500, phasor.Label, string.IsNullOrWhiteSpace(device.Name) ? device.Acronym ?? "" : device.Name);
                    phasor.DestinationPhasorID = null;
                    phasor.SourceIndex         = phasorDefinition.Index + 1;

                    phasorTable.AddNewPhasor(phasor);
                    SavePhasorMeasurement(isVoltage ? vphmSignalType : iphmSignalType, device, phasorDefinition, phasor.Phase, phasor.SourceIndex, phasor.BaseKV, measurementTable, scanParams);
                    SavePhasorMeasurement(isVoltage ? vphaSignalType : iphaSignalType, device, phasorDefinition, phasor.Phase, phasor.SourceIndex, phasor.BaseKV, measurementTable, scanParams);
                }
            }
            else
            {
                foreach (IPhasorDefinition phasorDefinition in cell.PhasorDefinitions)
                {
                    bool isVoltage = phasorDefinition.PhasorType == PhasorType.Voltage;

                    Phasor phasor = phasorTable.QueryPhasorForDevice(device.ID, phasorDefinition.Index + 1);
                    phasor.DeviceID = device.ID;
                    phasor.Label    = phasorDefinition.Label;
                    phasor.Type     = isVoltage ? 'V' : 'I';

                    phasorTable.AddNewPhasor(phasor);
                    SavePhasorMeasurement(isVoltage ? vphmSignalType : iphmSignalType, device, phasorDefinition, phasor.Phase, phasor.SourceIndex, phasor.BaseKV, measurementTable, scanParams);
                    SavePhasorMeasurement(isVoltage ? vphaSignalType : iphaSignalType, device, phasorDefinition, phasor.Phase, phasor.SourceIndex, phasor.BaseKV, measurementTable, scanParams);
                }
            }
        }
        private void SaveFixedMeasurement(SignalType signalType, Device device, TableOperations <Measurement> measurementTable, ScanParameters scanParams, string label = null)
        {
            string signalReference = $"{device.Acronym}-{signalType.Suffix}";

            // Query existing measurement record for specified signal reference - function will create a new blank measurement record if one does not exist
            Measurement measurement = measurementTable.QueryMeasurement(signalReference);
            string      pointTag    = scanParams.CreatePointTag(device.Acronym, signalType.Acronym);

            measurement.DeviceID        = device.ID;
            measurement.PointTag        = pointTag;
            measurement.Description     = $"{device.Acronym} {signalType.Name}{(string.IsNullOrWhiteSpace(label) ? "" : " - " + label)}";
            measurement.SignalReference = signalReference;
            measurement.SignalTypeID    = signalType.ID;
            measurement.Internal        = true;
            measurement.Enabled         = true;

            measurementTable.AddNewOrUpdateMeasurement(measurement);
        }
        private void SaveDeviceRecords(IConfigurationFrame configFrame, Device device, ScanParameters scanParams)
        {
            AdoDataConnection             connection       = scanParams.Connection;
            TableOperations <Measurement> measurementTable = new(connection);
            IConfigurationCell            cell             = configFrame.Cells[0];

            // Add frequency
            SaveFixedMeasurement(m_deviceSignalTypes["FREQ"], device, measurementTable, scanParams, cell.FrequencyDefinition.Label);

            // Add dF/dt
            SaveFixedMeasurement(m_deviceSignalTypes["DFDT"], device, measurementTable, scanParams);

            // Add status flags
            SaveFixedMeasurement(m_deviceSignalTypes["FLAG"], device, measurementTable, scanParams);

            // Add analogs
            SignalType analogSignalType = m_deviceSignalTypes["ALOG"];

            for (int i = 0; i < cell.AnalogDefinitions.Count; i++)
            {
                int index = i + 1;
                IAnalogDefinition analogDefinition = cell.AnalogDefinitions[i];
                string            signalReference  = $"{device.Acronym}-{analogSignalType.Suffix}{index}";

                // Query existing measurement record for specified signal reference - function will create a new blank measurement record if one does not exist
                Measurement measurement = measurementTable.QueryMeasurement(signalReference);
                string      pointTag    = scanParams.CreateIndexedPointTag(device.Acronym, analogSignalType.Acronym, index);
                measurement.DeviceID        = device.ID;
                measurement.PointTag        = pointTag;
                measurement.AlternateTag    = analogDefinition.Label;
                measurement.Description     = $"{device.Acronym} Analog Value {index} {analogDefinition.AnalogType}: {analogDefinition.Label}";
                measurement.SignalReference = signalReference;
                measurement.SignalTypeID    = analogSignalType.ID;
                measurement.Internal        = true;
                measurement.Enabled         = true;

                measurementTable.AddNewOrUpdateMeasurement(measurement);
            }

            // Add digitals
            SignalType digitalSignalType = m_deviceSignalTypes["DIGI"];

            for (int i = 0; i < cell.DigitalDefinitions.Count; i++)
            {
                int index = i + 1;
                IDigitalDefinition digitialDefinition = cell.DigitalDefinitions[i];
                string             signalReference    = $"{device.Acronym}-{digitalSignalType.Suffix}{index}";

                // Query existing measurement record for specified signal reference - function will create a new blank measurement record if one does not exist
                Measurement measurement = measurementTable.QueryMeasurement(signalReference);
                string      pointTag    = scanParams.CreateIndexedPointTag(device.Acronym, digitalSignalType.Acronym, index);
                measurement.DeviceID        = device.ID;
                measurement.PointTag        = pointTag;
                measurement.AlternateTag    = digitialDefinition.Label;
                measurement.Description     = $"{device.Acronym} Digital Value {index}: {digitialDefinition.Label}";
                measurement.SignalReference = signalReference;
                measurement.SignalTypeID    = digitalSignalType.ID;
                measurement.Internal        = true;
                measurement.Enabled         = true;

                measurementTable.AddNewOrUpdateMeasurement(measurement);
            }

            // Add phasors
            SaveDevicePhasors(cell, device, measurementTable, scanParams);
        }
        private void SaveDeviceConnection(IConfigurationFrame configFrame, string connectionString, ushort comPort, ushort idCode, ScanParameters scanParams)
        {
            TableOperations <Device> deviceTable = scanParams.DeviceTable;
            Guid nodeID = scanParams.NodeID;

            ShowUpdateMessage($"{Tab2}Saving device connection...");

            // Query existing device record, creating new one if not found
            Device device = scanParams.Devices.FindDeviceByComPort(comPort) ?? deviceTable.NewDevice();
            bool   skipDisableRealTimeData;

            // Handle connection string parameters that are fields in the device table
            (skipDisableRealTimeData, connectionString) = ExtractFieldAssignedSkipDisableRealTimeData(connectionString);

            IConfigurationCell deviceConfig = configFrame.Cells[0];

            string deviceAcronym = deviceConfig.IDLabel;
            string deviceName    = null;

            if (string.IsNullOrWhiteSpace(deviceAcronym) && !string.IsNullOrWhiteSpace(deviceConfig.StationName))
            {
                deviceAcronym = GetCleanAcronym(deviceConfig.StationName.ToUpperInvariant().Replace(" ", "_"));
            }
            else
            {
                throw new InvalidOperationException("Unable to get station name or ID label from device configuration frame");
            }

            if (!string.IsNullOrWhiteSpace(deviceConfig.StationName))
            {
                deviceName = deviceConfig.StationName;
            }

            device.NodeID                       = nodeID;
            device.Acronym                      = deviceAcronym;
            device.Name                         = deviceName ?? deviceAcronym;
            device.ProtocolID                   = scanParams.IeeeC37_118ProtocolID;
            device.FramesPerSecond              = configFrame.FrameRate;
            device.AccessID                     = idCode;
            device.IsConcentrator               = false;
            device.ConnectionString             = connectionString;
            device.AutoStartDataParsingSequence = true;
            device.SkipDisableRealTimeData      = skipDisableRealTimeData;
            device.Enabled                      = true;

            // Check if this is a new device or an edit to an existing one
            if (device.ID == 0)
            {
                // Add new device record
                deviceTable.AddNewDevice(device);

                // Get newly added device with auto-incremented ID
                Device newDevice = deviceTable.QueryDevice(device.Acronym);

                // Save associated device records
                SaveDeviceRecords(configFrame, newDevice, scanParams);
            }
            else
            {
                // Update existing device record
                deviceTable.UpdateDevice(device);

                // Save associated device records
                SaveDeviceRecords(configFrame, device, scanParams);
            }
        }
        private bool SaveDeviceConfiguration(IConfigurationFrame configFrame, ushort comPort, ushort idCode, ScanParameters scanParams)
        {
            try
            {
                AdoDataConnection            connection      = scanParams.Connection;
                TableOperations <SignalType> signalTypeTable = new(connection);
                string configConnectionMode = scanParams.ControllingConnection ? ControllingConnectionString : ListeningConnectionString;
                string connectionString     = string.Format(ConnectionStringTemplate, comPort, Settings.BaudRate, Settings.Parity, Settings.StopBits, Settings.DataBits, Settings.DtrEnable, Settings.RtsEnable, configConnectionMode);

                ShowUpdateMessage($"{Tab2}Saving \"{configFrame.Cells[0].StationName}\" configuration received on COM{comPort} with ID code {idCode}...");

                m_deviceSignalTypes ??= signalTypeTable.LoadSignalTypes("PMU").ToDictionary(key => key.Acronym, StringComparer.OrdinalIgnoreCase);
                m_phasorSignalTypes ??= signalTypeTable.LoadSignalTypes("Phasor").ToDictionary(key => key.Acronym, StringComparer.OrdinalIgnoreCase);

                SaveDeviceConnection(configFrame, connectionString, comPort, idCode, scanParams);

                return(true);
            }
            catch (Exception ex)
            {
                ShowUpdateMessage($"{Tab2}ERROR: Failed while saving \"{configFrame.Cells[0].StationName}\" configuration: {ex.Message}");
                m_log.Publish(MessageLevel.Error, nameof(AutoConfigPortScanner), exception: ex);

                return(false);
            }
        }