namespace ZB.MOM.WW.OtOpcUa.Driver.S7; /// /// Siemens S7 memory area. The driver's tag-address parser maps every S7 tag string into /// exactly one of these + an offset. Values match the on-wire S7 area codes only /// incidentally — S7.Net uses its own DataType enum (DataBlock, Memory, /// Input, Output, Timer, Counter) so the adapter layer translates. /// public enum S7Area { DataBlock, Memory, // M (Merker / marker byte) Input, // I (process-image input) Output, // Q (process-image output) Timer, Counter, } /// /// Access width for a DB / M / I / Q address. Timers and counters are always 16-bit /// opaque (not user-addressable via size suffixes). /// public enum S7Size { Bit, // X Byte, // B Word, // W — 16-bit DWord, // D — 32-bit } /// /// Parsed form of an S7 tag-address string. Produced by . /// /// Memory area (DB, M, I, Q, T, C). /// Data block number; only meaningful when is . /// Access width. Always for Timer and Counter. /// Byte offset into the area (for DB/M/I/Q) or the timer/counter number. /// Bit position 0-7 when is ; 0 otherwise. public readonly record struct S7ParsedAddress( S7Area Area, int DbNumber, S7Size Size, int ByteOffset, int BitOffset); /// /// Parses Siemens S7 address strings into . Accepts the /// Siemens TIA-Portal / STEP 7 Classic syntax documented in docs/v2/driver-specs.md §5: /// /// DB{n}.DB{X|B|W|D}{offset}[.bit] — e.g. DB1.DBX0.0, DB1.DBW0, DB1.DBD4 /// M{B|W|D}{offset} or M{offset}.{bit} — e.g. MB0, MW0, MD4, M0.0 /// I{B|W|D}{offset} or I{offset}.{bit} — e.g. IB0, IW0, ID0, I0.0 /// Q{B|W|D}{offset} or Q{offset}.{bit} — e.g. QB0, QW0, QD0, Q0.0 /// T{n} — e.g. T0, T15 /// C{n} — e.g. C0, C10 /// /// Grammar is case-insensitive. Leading/trailing whitespace tolerated. Bit specifiers /// must be 0-7; byte offsets must be non-negative; DB numbers must be >= 1. /// /// /// Parse is deliberately strict — the parser rejects syntactic garbage up-front so a bad /// tag config fails at driver init time instead of surfacing as a misleading /// BadInternalError on every Read against that tag. /// public static class S7AddressParser { /// /// Parse an S7 address. Throws on any syntax error with /// the offending input echoed in the message so operators can correlate to the tag /// config that produced the fault. /// public static S7ParsedAddress Parse(string address) { if (string.IsNullOrWhiteSpace(address)) throw new FormatException("S7 address must not be empty"); var s = address.Trim().ToUpperInvariant(); // --- DB{n}.DB{X|B|W|D}{offset}[.bit] --- if (s.StartsWith("DB") && TryParseDataBlock(s, out var dbResult)) return dbResult; if (s.Length < 2) throw new FormatException($"S7 address '{address}' is too short to parse"); var areaChar = s[0]; var rest = s.Substring(1); switch (areaChar) { case 'M': return ParseMIQ(S7Area.Memory, rest, address); case 'I': return ParseMIQ(S7Area.Input, rest, address); case 'Q': return ParseMIQ(S7Area.Output, rest, address); case 'T': return ParseTimerOrCounter(S7Area.Timer, rest, address); case 'C': return ParseTimerOrCounter(S7Area.Counter, rest, address); default: throw new FormatException($"S7 address '{address}' starts with unknown area '{areaChar}' (expected DB/M/I/Q/T/C)"); } } /// /// Try-parse variant for callers that can't afford an exception on bad input (e.g. /// config validation pages in the Admin UI). Returns false for any input that /// would throw from . /// public static bool TryParse(string address, out S7ParsedAddress result) { try { result = Parse(address); return true; } catch (FormatException) { result = default; return false; } } private static bool TryParseDataBlock(string s, out S7ParsedAddress result) { result = default; // Split on first '.': left side must be DB{n}, right side DB{X|B|W|D}{offset}[.bit] var dot = s.IndexOf('.'); if (dot < 0) return false; var head = s.Substring(0, dot); // DB{n} var tail = s.Substring(dot + 1); // DB{X|B|W|D}{offset}[.bit] if (head.Length < 3) return false; if (!int.TryParse(head.AsSpan(2), out var dbNumber) || dbNumber < 1) throw new FormatException($"S7 DB number in '{s}' must be a positive integer"); if (!tail.StartsWith("DB") || tail.Length < 4) throw new FormatException($"S7 DB address tail '{tail}' must start with DB{{X|B|W|D}}"); var sizeChar = tail[2]; var offsetStart = 3; var size = sizeChar switch { 'X' => S7Size.Bit, 'B' => S7Size.Byte, 'W' => S7Size.Word, 'D' => S7Size.DWord, _ => throw new FormatException($"S7 DB size '{sizeChar}' in '{s}' must be X/B/W/D"), }; var (byteOffset, bitOffset) = ParseOffsetAndOptionalBit(tail, offsetStart, size, s); result = new S7ParsedAddress(S7Area.DataBlock, dbNumber, size, byteOffset, bitOffset); return true; } private static S7ParsedAddress ParseMIQ(S7Area area, string rest, string original) { if (rest.Length == 0) throw new FormatException($"S7 address '{original}' has no offset"); var first = rest[0]; S7Size size; int offsetStart; switch (first) { case 'B': size = S7Size.Byte; offsetStart = 1; break; case 'W': size = S7Size.Word; offsetStart = 1; break; case 'D': size = S7Size.DWord; offsetStart = 1; break; default: // No size prefix => bit-level address requires explicit .bit. Size stays Bit; // ParseOffsetAndOptionalBit will demand the dot. size = S7Size.Bit; offsetStart = 0; break; } var (byteOffset, bitOffset) = ParseOffsetAndOptionalBit(rest, offsetStart, size, original); return new S7ParsedAddress(area, DbNumber: 0, size, byteOffset, bitOffset); } private static S7ParsedAddress ParseTimerOrCounter(S7Area area, string rest, string original) { if (rest.Length == 0) throw new FormatException($"S7 address '{original}' has no {area} number"); if (!int.TryParse(rest, out var number) || number < 0) throw new FormatException($"S7 {area} number in '{original}' must be a non-negative integer"); return new S7ParsedAddress(area, DbNumber: 0, S7Size.Word, number, BitOffset: 0); } private static (int byteOffset, int bitOffset) ParseOffsetAndOptionalBit( string s, int start, S7Size size, string original) { var offsetEnd = start; while (offsetEnd < s.Length && s[offsetEnd] >= '0' && s[offsetEnd] <= '9') offsetEnd++; if (offsetEnd == start) throw new FormatException($"S7 address '{original}' has no byte-offset digits"); if (!int.TryParse(s.AsSpan(start, offsetEnd - start), out var byteOffset) || byteOffset < 0) throw new FormatException($"S7 byte offset in '{original}' must be non-negative"); // No bit-suffix: done unless size is Bit with no prefix, which requires one. if (offsetEnd == s.Length) { if (size == S7Size.Bit) throw new FormatException($"S7 address '{original}' needs a .{{bit}} suffix for bit access"); return (byteOffset, 0); } if (s[offsetEnd] != '.') throw new FormatException($"S7 address '{original}' has unexpected character after offset"); if (size != S7Size.Bit) throw new FormatException($"S7 address '{original}' has a bit suffix but the size is {size} — bit access needs X (DB) or no size prefix (M/I/Q)"); if (!int.TryParse(s.AsSpan(offsetEnd + 1), out var bitOffset) || bitOffset is < 0 or > 7) throw new FormatException($"S7 bit offset in '{original}' must be 0-7"); return (byteOffset, bitOffset); } }