using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Siemens.Simatic.Simulation.Runtime; using Newtonsoft.Json; using System.Text.RegularExpressions; using System.ComponentModel; using Newtonsoft.Json.Linq; using System.ComponentModel.DataAnnotations; using System.Diagnostics; namespace LibS7Adv { [DisplayName("PLC Advanced Setup:")] public partial class PlcData : ObservableObject { [ObservableProperty] [property: Description("Type of connection driver")] [property: Category("Connection:")] ConnectionType connectionType = ConnectionType.AdvCoSimulator; [ObservableProperty] [property: Description("IP of the PLC")] [property: Category("Connection:")] string iP = "10.1.30.11"; [ObservableProperty] [property: Description("Name of the instance at the PLC Advanced")] [property: Category("Connection:")] string name = "PLC Name"; [ObservableProperty] [property: Description("CPU Info")] [property: Category("Status:")] [property: ReadOnly(true)] string cpuTime = ""; [ObservableProperty] [property: Display(Name = "Status")] [property: Description("CPU Status")] [property: Category("Status:")] [property: ReadOnly(true)] string connectionStatus = ""; [ObservableProperty] [property: Description("API Error")] [property: Category("Status:")] [property: ReadOnly(true)] string lastError = ""; } public partial class PLCViewModel : ObservableObject { [JsonIgnore] private IPlcConnection? _connection; [ObservableProperty] PlcData plcData = new PlcData(); public PLCViewModel() { // Configurar la conexión inicial _connection = CreateConnection(PlcData.ConnectionType); } private IPlcConnection CreateConnection(ConnectionType connectionType) { try { return connectionType switch { ConnectionType.AdvCoSimulator => new AdvCoSimulatorConnection(), ConnectionType.Sharp7 => new Sharp7Connection(), _ => throw new ArgumentException("Invalid connection type") }; } catch (Exception ex) { PlcData.LastError = $"Error creating {connectionType} connection: {ex.Message}"; // Fallback a AdvCoSimulator si hay problemas con Snap7 if (connectionType == ConnectionType.Sharp7) { PlcData.LastError += " Falling back to AdvCoSimulator."; PlcData.ConnectionType = ConnectionType.AdvCoSimulator; return new AdvCoSimulatorConnection(); } throw; } } partial void OnPlcDataChanged(PlcData value) { // Si cambió el tipo de conexión, recreamos la conexión if (_connection?.ConnectionType != value.ConnectionType) { if (_connection?.IsConnected == true) { Disconnect(); } _connection = CreateConnection(value.ConnectionType); } } public void ucLoaded() { IsConnected = false; PlcData.LastError = ""; PlcData.ConnectionStatus = "offline"; } [ObservableProperty] [property: JsonIgnore] bool isConnected; [RelayCommand] [property: JsonIgnore] public void ResetAlarmButton() { PlcData.LastError = ""; if (_connection != null) { PlcData.LastError = _connection.LastError; } } [RelayCommand] [property: JsonIgnore] public void ConnectButton() { if (IsConnected) Disconnect(); else Connect(); } public void Connect() { if (IsConnected) return; try { if (_connection == null) { _connection = CreateConnection(PlcData.ConnectionType); } // Verificación especial para Snap7 // Para Sharp7 no necesitamos validar DLL externa ya que es una librería C# administrada bool success = _connection.Connect(PlcData.IP, PlcData.Name); if (success) { IsConnected = true; PlcData.ConnectionStatus = _connection.Status; PlcData.LastError = ""; } else { IsConnected = false; PlcData.ConnectionStatus = "offline"; PlcData.LastError = _connection.LastError; } } catch (Exception ex) { PlcData.LastError = ex.Message; PlcData.ConnectionStatus = "offline"; IsConnected = false; } } public void Disconnect() { _connection?.Disconnect(); IsConnected = false; PlcData.ConnectionStatus = "offline"; } /// /// Obtiene información de diagnóstico sobre los drivers disponibles /// /// Información de estado de los drivers public string GetDriverDiagnostics() { var diagnostics = new System.Text.StringBuilder(); diagnostics.AppendLine("=== Driver Diagnostics ==="); diagnostics.AppendLine($"Current Driver: {PlcData.ConnectionType}"); diagnostics.AppendLine($"Connection Status: {PlcData.ConnectionStatus}"); diagnostics.AppendLine($"Last Error: {PlcData.LastError}"); diagnostics.AppendLine(); // Diagnóstico AdvCoSimulator diagnostics.AppendLine("AdvCoSimulator Status:"); try { var advConnection = new AdvCoSimulatorConnection(); diagnostics.AppendLine("✓ AdvCoSimulator driver available"); } catch (Exception ex) { diagnostics.AppendLine($"✗ AdvCoSimulator error: {ex.Message}"); } diagnostics.AppendLine(); // Diagnóstico Sharp7 diagnostics.AppendLine("Sharp7 Status:"); diagnostics.AppendLine(" Status: ✓ C# Managed Library (no external DLL required)"); diagnostics.AppendLine(" Available: ✓ Yes"); return diagnostics.ToString(); } /// /// Mapea un tag a su dirección correspondiente para Sharp7. /// IMPORTANTE: Esta función solo se debe usar con Sharp7, NO con AdvCoSimulator. /// - Para Sharp7: Normaliza diferentes notaciones de direcciones PLC y retorna formato compatible. /// - Para AdvCoSimulator: No usar esta función, usar los tags directamente. /// /// Soporta múltiples notaciones: /// - Alemán vs Americano: E/A vs I/O /// - Con/Sin P: PEW vs EW, PIW vs IW /// - Diferentes formatos: IW0, PIW0, EW0, PEW0 /// /// En el futuro se puede implementar una tabla de mapeo para convertir tags simbólicos a direcciones absolutas. /// /// El tag o dirección a mapear (solo para Sharp7) /// La dirección absoluta correspondiente normalizada para Sharp7 /// /// Ejemplos de normalización: /// /// "PEW0" → "IW0" (Alemán con P → Americano) /// "EW0" → "IW0" (Alemán → Americano) /// "PIW0" → "IW0" (Con P → Sin P) /// "PAW0" → "QW0" (Alemán con P → Americano) /// "AW0" → "QW0" (Alemán → Americano) /// "POW0" → "QW0" (Con P → Sin P) /// /// /// Ejemplo de uso futuro con tabla de mapeo: /// /// // Implementación futura: /// private Dictionary<string, string> _tagMappingTable = new Dictionary<string, string> /// { /// { "MotorStart", "DB1.DBX0.0" }, /// { "MotorSpeed", "DB1.DBW2" }, /// { "Temperature", "DB2.DBD4" } /// }; /// /// private string MapTagToAddress(string tag) { // TODO: En el futuro, implementar búsqueda en tabla de mapeo // if (_tagMappingTable?.ContainsKey(tag) == true) // { // return _tagMappingTable[tag]; // } // Normalizar direcciones PLC para compatibilidad con Sharp7 string normalizedAddress = NormalizePlcAddress(tag); return normalizedAddress; } /// /// Normaliza direcciones PLC de diferentes notaciones a formato compatible con Sharp7 /// /// Dirección original en cualquier formato /// Dirección normalizada para Sharp7 private string NormalizePlcAddress(string address) { if (string.IsNullOrEmpty(address)) return address; // Convertir a mayúsculas para normalización string upperAddress = address.ToUpper(); // === NORMALIZACIÓN DE NOTACIONES INPUTS/OUTPUTS === // PEW (Alemán con P) → IW (Americano) if (upperAddress.StartsWith("PEW")) return upperAddress.Replace("PEW", "IW"); // PIW (Americano con P) → IW (Americano sin P) if (upperAddress.StartsWith("PIW")) return upperAddress.Replace("PIW", "IW"); // EW (Alemán) → IW (Americano) if (upperAddress.StartsWith("EW")) return upperAddress.Replace("EW", "IW"); // PEB (Alemán con P) → IB (Americano) if (upperAddress.StartsWith("PEB")) return upperAddress.Replace("PEB", "IB"); // PIB (Americano con P) → IB (Americano sin P) if (upperAddress.StartsWith("PIB")) return upperAddress.Replace("PIB", "IB"); // EB (Alemán) → IB (Americano) if (upperAddress.StartsWith("EB")) return upperAddress.Replace("EB", "IB"); // PED (Alemán con P) → ID (Americano) if (upperAddress.StartsWith("PED")) return upperAddress.Replace("PED", "ID"); // PID (Americano con P) → ID (Americano sin P) if (upperAddress.StartsWith("PID")) return upperAddress.Replace("PID", "ID"); // ED (Alemán) → ID (Americano) if (upperAddress.StartsWith("ED")) return upperAddress.Replace("ED", "ID"); // === OUTPUTS === // PAW (Alemán con P) → QW (Americano) if (upperAddress.StartsWith("PAW")) return upperAddress.Replace("PAW", "QW"); // POW (Americano con P) → QW (Americano sin P) if (upperAddress.StartsWith("POW")) return upperAddress.Replace("POW", "QW"); // AW (Alemán) → QW (Americano) if (upperAddress.StartsWith("AW")) return upperAddress.Replace("AW", "QW"); // PAB (Alemán con P) → QB (Americano) if (upperAddress.StartsWith("PAB")) return upperAddress.Replace("PAB", "QB"); // POB (Americano con P) → QB (Americano sin P) if (upperAddress.StartsWith("POB")) return upperAddress.Replace("POB", "QB"); // AB (Alemán) → QB (Americano) if (upperAddress.StartsWith("AB")) return upperAddress.Replace("AB", "QB"); // PAD (Alemán con P) → QD (Americano) if (upperAddress.StartsWith("PAD")) return upperAddress.Replace("PAD", "QD"); // POD (Americano con P) → QD (Americano sin P) if (upperAddress.StartsWith("POD")) return upperAddress.Replace("POD", "QD"); // AD (Alemán) → QD (Americano) if (upperAddress.StartsWith("AD")) return upperAddress.Replace("AD", "QD"); // === BITS (con punto) === // PE0.0 (Alemán con P) → I0.0 (Americano) if (upperAddress.StartsWith("PE") && upperAddress.Contains(".")) return upperAddress.Replace("PE", "I"); // PI0.0 (Americano con P) → I0.0 (Americano sin P) if (upperAddress.StartsWith("PI") && upperAddress.Contains(".")) return upperAddress.Replace("PI", "I"); // E0.0 (Alemán) → I0.0 (Americano) if (upperAddress.StartsWith("E") && upperAddress.Contains(".") && !upperAddress.StartsWith("EB") && !upperAddress.StartsWith("EW") && !upperAddress.StartsWith("ED")) return upperAddress.Replace("E", "I"); // PA0.0 (Alemán con P) → Q0.0 (Americano) if (upperAddress.StartsWith("PA") && upperAddress.Contains(".")) return upperAddress.Replace("PA", "Q"); // PO0.0 (Americano con P) → Q0.0 (Americano sin P) if (upperAddress.StartsWith("PO") && upperAddress.Contains(".")) return upperAddress.Replace("PO", "Q"); // A0.0 (Alemán) → Q0.0 (Americano) if (upperAddress.StartsWith("A") && upperAddress.Contains(".") && !upperAddress.StartsWith("AB") && !upperAddress.StartsWith("AW") && !upperAddress.StartsWith("AD")) return upperAddress.Replace("A", "Q"); // Si no necesita normalización, devolver tal como está return upperAddress; } public bool? LeerBool(string sTag) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(sTag) : sTag; var result = _connection?.ReadBool(address); if (_connection != null) { PlcData.LastError = _connection.LastError; } return result; } catch (Exception ex) { PlcData.LastError = $"{sTag}: {ex.Message}"; return false; } } public bool? EscribirBool(string sTag, bool Value) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(sTag) : sTag; var result = _connection?.WriteBool(address, Value); if (_connection != null) { PlcData.LastError = _connection.LastError; } return result; } catch (Exception ex) { PlcData.LastError = $"{sTag}: {ex.Message}"; return false; } } public string? LeerNumber(string sTag, bool Signed = false) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(sTag) : sTag; var result = _connection?.ReadNumber(address, Signed); if (_connection != null) { PlcData.LastError = _connection.LastError; } return result; } catch (Exception ex) { PlcData.LastError = $"{sTag}: {ex.Message}"; return ""; } } public bool EscribirNumber(string sTag, object value) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(sTag) : sTag; var result = _connection?.WriteNumber(address, value) ?? false; if (_connection != null) { PlcData.LastError = _connection.LastError; } return result; } catch (Exception ex) { PlcData.LastError = $"{sTag}: {ex.Message}"; return false; } } public bool LeerSalidaBool(byte pByte, int pBit) { try { var result = _connection?.ReadOutputBool(pByte, pBit) ?? false; if (_connection != null) { PlcData.LastError = _connection.LastError; } return result; } catch (Exception ex) { PlcData.LastError = $"Byte {pByte}, Bit {pBit}: {ex.Message}"; return false; } } public void EscribirInputBool(byte pByte, int pBit, bool pValue) { try { _connection?.WriteInputBool(pByte, pBit, pValue); if (_connection != null) { PlcData.LastError = _connection.LastError; } } catch (Exception ex) { PlcData.LastError = $"Byte {pByte}, Bit {pBit}: {ex.Message}"; } } public void EscribirTag(string pTag, SDataValue Value) { try { if (_connection?.ConnectionType == ConnectionType.AdvCoSimulator) { // AdvCoSimulator: usar tag directamente con SDataValue _connection?.WriteTag(pTag, Value); if (_connection != null) { PlcData.LastError = _connection.LastError; } } else if (_connection?.ConnectionType == ConnectionType.Sharp7) { // Sharp7: mapear tag y convertir SDataValue a función específica string address = MapTagToAddress(pTag); // Detectar el tipo del SDataValue y usar la función correspondiente switch (Value.Type) { case EPrimitiveDataType.Bool: EscribirBool(address, Value.Bool); break; case EPrimitiveDataType.Int8: case EPrimitiveDataType.UInt8: EscribirNumber(address, Value.UInt8); break; case EPrimitiveDataType.Int16: EscribirNumber(address, Value.Int16); break; case EPrimitiveDataType.UInt16: EscribirNumber(address, Value.UInt16); break; case EPrimitiveDataType.Int32: EscribirNumber(address, Value.Int32); break; case EPrimitiveDataType.UInt32: EscribirNumber(address, Value.UInt32); break; case EPrimitiveDataType.Float: EscribirNumber(address, Value.Float); break; case EPrimitiveDataType.Double: EscribirNumber(address, Value.Double); break; default: PlcData.LastError = $"Unsupported SDataValue type: {Value.Type} for Sharp7"; break; } } else { PlcData.LastError = "No connection available"; } } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; } } public SDataValue LeerTag(string pTag) { try { if (_connection?.ConnectionType == ConnectionType.AdvCoSimulator) { // AdvCoSimulator: usar tag directamente con SDataValue var result = _connection?.ReadTag(pTag); if (_connection != null) { PlcData.LastError = _connection.LastError; } return result as SDataValue? ?? new SDataValue(); } else if (_connection?.ConnectionType == ConnectionType.Sharp7) { // Sharp7: no soporta SDataValue directamente // El usuario debería usar funciones específicas como LeerBool, LeerNumber PlcData.LastError = "LeerTag with SDataValue is not supported with Sharp7. Use specific type methods like LeerBool, LeerNumber."; return new SDataValue(); } else { PlcData.LastError = "No connection available"; return new SDataValue(); } } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; return new SDataValue(); } } public void EscribirTagBool(string pTag, bool pValue) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(pTag) : pTag; // Usar directamente la función unificada EscribirBool EscribirBool(address, pValue); } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; } } public void EscribirTagInt16(string pTag, int pValue) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(pTag) : pTag; // Usar directamente la función unificada EscribirNumber EscribirNumber(address, pValue); } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; } } public bool LeerTagBool(string pTag) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(pTag) : pTag; // Usar directamente la función unificada LeerBool return LeerBool(address) ?? false; } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; return false; } } public int? LeerTagInt16(string pTag) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(pTag) : pTag; // Usar directamente la función unificada LeerNumber string? result = LeerNumber(address); if (int.TryParse(result, out int value)) { return value; } return null; } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; return null; } } public int? LeerTagDInt(string pTag) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(pTag) : pTag; // Usar directamente la función unificada LeerNumber string? result = LeerNumber(address); if (int.TryParse(result, out int value)) { return value; } return null; } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; return null; } } public void EscribirTagDInt(string pTag, int pValue) { try { // Para Sharp7: mapear tag a dirección absoluta // Para AdvCoSimulator: usar tag directamente string address = _connection?.ConnectionType == ConnectionType.Sharp7 ? MapTagToAddress(pTag) : pTag; // Usar directamente la función unificada EscribirNumber EscribirNumber(address, pValue); } catch (Exception ex) { PlcData.LastError = $"{pTag}: {ex.Message}"; } } public static TagAddress? ParseTagAddress(string tag) { TagAddress tagAddress = new TagAddress(); if (tag == null) return null; if (tag.StartsWith("%")) { if (tag.StartsWith("%DB")) { tagAddress.areaType = EArea.DataBlock; var match = Regex.Match(tag, @"^%DB(\d+)\.DB([X|B|W|D])(\d+)\.(\d+)$"); if (match.Success) { tagAddress.DB = int.Parse(match.Groups[1].Value); char dataTypeChar = match.Groups[2].Value[0]; tagAddress.word_offset = uint.Parse(match.Groups[3].Value); tagAddress.bit = byte.Parse(match.Groups[4].Value); switch (dataTypeChar) { case 'X': tagAddress.tagType = EDataType.Bool; break; case 'B': tagAddress.tagType = EDataType.Byte; break; case 'W': tagAddress.tagType = EDataType.Word; break; case 'D': tagAddress.tagType = EDataType.DWord; break; default: tagAddress.tagType = EDataType.Unknown; break; } } } else if (tag.StartsWith("%M")) { tagAddress.areaType = EArea.Marker; ParseNonDBAddress(tag, tagAddress); } else if (tag.StartsWith("%E")) { tagAddress.areaType = EArea.Input; ParseNonDBAddress(tag, tagAddress); } else if (tag.StartsWith("%A")) { tagAddress.areaType = EArea.Output; ParseNonDBAddress(tag, tagAddress); } else { tagAddress.areaType = EArea.InvalidArea; } } else { // If it's not a direct address, treat it as a tag name tagAddress.areaType = EArea.InvalidArea; tagAddress.tagType = EDataType.Unknown; } return tagAddress; } private static void ParseNonDBAddress(string tag, TagAddress tagAddress) { var match = Regex.Match(tag, @"^%[EMA](\d+)\.(\d+)$"); if (match.Success) { tagAddress.word_offset = uint.Parse(match.Groups[1].Value); tagAddress.bit = byte.Parse(match.Groups[2].Value); tagAddress.tagType = EDataType.Bool; } else { match = Regex.Match(tag, @"^%[EMA]([XBWDF])(\d+)$"); if (match.Success) { char dataTypeChar = match.Groups[1].Value[0]; tagAddress.word_offset = uint.Parse(match.Groups[2].Value); switch (dataTypeChar) { case 'X': tagAddress.tagType = EDataType.Bool; break; case 'B': tagAddress.tagType = EDataType.Byte; break; case 'W': tagAddress.tagType = EDataType.Word; break; case 'D': tagAddress.tagType = EDataType.DWord; break; default: tagAddress.tagType = EDataType.Unknown; break; } } } } } public class TagAddress { public EArea areaType; public EDataType tagType; public int DB; public uint word_offset; public byte bit; } }