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: mapear tag y determinar el tipo basado en la dirección
string address = MapTagToAddress(pTag);
// Detectar el tipo de datos basado en la dirección para crear el SDataValue apropiado
var tagAddress = ParseTagAddress("%" + address); // Agregar % para el parser
if (tagAddress != null)
{
switch (tagAddress.tagType)
{
case EDataType.Bool:
var boolResult = LeerBool(address);
if (boolResult.HasValue)
{
return new SDataValue { Bool = boolResult.Value };
}
break;
case EDataType.Byte:
var byteResult = LeerNumber(address, false);
if (byte.TryParse(byteResult, out byte byteValue))
{
return new SDataValue { UInt8 = byteValue };
}
break;
case EDataType.Word:
var wordResult = LeerNumber(address, false);
if (ushort.TryParse(wordResult, out ushort wordValue))
{
return new SDataValue { UInt16 = wordValue };
}
break;
case EDataType.DWord:
var dwordResult = LeerNumber(address, false);
if (uint.TryParse(dwordResult, out uint dwordValue))
{
return new SDataValue { UInt32 = dwordValue };
}
break;
default:
// Si no se puede determinar el tipo, intentar como string/número
var defaultResult = LeerNumber(address, false);
if (int.TryParse(defaultResult, out int intValue))
{
return new SDataValue { Int32 = intValue };
}
break;
}
}
else
{
// Si no se puede parsear la dirección, intentar diferentes tipos
// Primero intentar como bool si la dirección tiene formato de bit
if (address.Contains("."))
{
var boolResult = LeerBool(address);
if (boolResult.HasValue)
{
return new SDataValue { Bool = boolResult.Value };
}
}
else
{
// Intentar como número
var numberResult = LeerNumber(address, false);
if (int.TryParse(numberResult, out int intValue))
{
return new SDataValue { Int32 = intValue };
}
}
}
PlcData.LastError = $"Could not determine data type for address: {address}";
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;
}
}