using System.Buffers.Binary; using System.Text; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Decoder for the CIP Symbol Object (class 0x6B) response returned by Logix controllers /// when a client reads the @tags pseudo-tag. Parses the concatenated tag-info /// entries into a sequence of s that the driver can stream /// into the address-space builder. /// /// /// Entry layout (little-endian) per Rockwell CIP Vol 1 + Logix 5000 CIP Programming /// Manual (1756-PM019 chapter "Symbol Object"), cross-checked against libplctag's /// ab/cip.c handle_listed_tags_reply: /// /// u32Symbol Instance ID — opaque identifier for the tag. /// u16Symbol Type — lower 12 bits = CIP type code (0xC1 BOOL, /// 0xC2 SINT, …, 0xD0 STRING). Bit 12 = system-tag flag. Bit 13 = reserved. /// Bit 15 = struct flag; when set, the lower 12 bits are the template instance id /// (not a primitive type code). /// u16Element length — bytes per element (e.g. 4 for DINT). /// u32 × 3Array dimensions — zero for scalar tags. /// u16Symbol name length in bytes. /// u8 × NASCII symbol name, padded to an even byte boundary. /// /// /// Program:-scope tags arrive with their scope prefix baked into the name /// (Program:MainProgram.StepIndex); decoder strips the prefix + emits the scope /// separately so the driver's IAddressSpaceBuilder can organise them. /// public static class CipSymbolObjectDecoder { // Fixed header size in bytes — instance-id(4) + symbol-type(2) + element-length(2) // + array-dims(4×3) + name-length(2) = 22. private const int FixedHeaderSize = 22; private const ushort SymbolTypeSystemFlag = 0x1000; private const ushort SymbolTypeStructFlag = 0x8000; private const ushort SymbolTypeTypeCodeMask = 0x0FFF; /// /// Decode the raw @tags blob into an enumerable sequence. Malformed entries at /// the tail cause decoding to stop gracefully — the caller gets whatever it could parse /// cleanly before the corruption. /// public static IEnumerable Decode(byte[] buffer) { ArgumentNullException.ThrowIfNull(buffer); return DecodeImpl(buffer); } private static IEnumerable DecodeImpl(byte[] buffer) { var pos = 0; while (pos + FixedHeaderSize <= buffer.Length) { var instanceId = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(pos)); var symbolType = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(pos + 4)); // element_length at pos+6 (u16) — useful for array sizing but not surfaced here // array_dims at pos+8, pos+12, pos+16 — same (scalar-tag case has all zeros) var nameLength = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(pos + 20)); pos += FixedHeaderSize; if (pos + nameLength > buffer.Length) break; var name = Encoding.ASCII.GetString(buffer, pos, nameLength); pos += nameLength; if ((pos & 1) != 0) pos++; // even-align for the next entry if (string.IsNullOrWhiteSpace(name)) continue; var isSystem = (symbolType & SymbolTypeSystemFlag) != 0; var isStruct = (symbolType & SymbolTypeStructFlag) != 0; var typeCode = symbolType & SymbolTypeTypeCodeMask; var (programScope, simpleName) = SplitProgramScope(name); var dataType = isStruct ? AbCipDataType.Structure : MapTypeCode((byte)typeCode); yield return new AbCipDiscoveredTag( Name: simpleName, ProgramScope: programScope, DataType: dataType ?? AbCipDataType.Structure, // unknown type code → treat as opaque ReadOnly: false, // Symbol Object doesn't carry write-protection bits; lift via AccessControl Object later IsSystemTag: isSystem); _ = instanceId; // retained in the wire format for diagnostics; not surfaced to the driver today } } /// /// Split a Program:MainProgram.StepIndex-style name into its scope + local /// parts. Names without the Program: prefix pass through unchanged. /// internal static (string? programScope, string simpleName) SplitProgramScope(string fullName) { const string prefix = "Program:"; if (!fullName.StartsWith(prefix, StringComparison.Ordinal)) return (null, fullName); var afterPrefix = fullName[prefix.Length..]; var dot = afterPrefix.IndexOf('.'); if (dot <= 0) return (null, fullName); // malformed scope — surface the raw name return (afterPrefix[..dot], afterPrefix[(dot + 1)..]); } /// /// Map a CIP atomic type code (lower 12 bits of the symbol-type field) to our /// surface. Returns null for unrecognised codes — /// caller treats those as so the symbol is still /// surfaced + downstream config can add a concrete type override. /// internal static AbCipDataType? MapTypeCode(byte typeCode) => typeCode switch { 0xC1 => AbCipDataType.Bool, 0xC2 => AbCipDataType.SInt, 0xC3 => AbCipDataType.Int, 0xC4 => AbCipDataType.DInt, 0xC5 => AbCipDataType.LInt, 0xC6 => AbCipDataType.USInt, 0xC7 => AbCipDataType.UInt, 0xC8 => AbCipDataType.UDInt, 0xC9 => AbCipDataType.ULInt, 0xCA => AbCipDataType.Real, 0xCB => AbCipDataType.LReal, 0xCD => AbCipDataType.Dt, // DATE 0xCF => AbCipDataType.Dt, // DATE_AND_TIME 0xD0 => AbCipDataType.String, _ => null, }; }