namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// /// Parsed PCCC file-based address: file letter + file number + word number, optionally a /// sub-element (.ACC on a timer) or bit index (/0 on a bit file). /// /// /// Logix symbolic tags are parsed elsewhere ( is for SLC / PLC-5 / /// MicroLogix — no symbol table; everything is file-letter + file-number + word-number). /// /// N7:0 — integer file 7, word 0 (signed 16-bit). /// N7:5 — integer file 7, word 5. /// F8:0 — float file 8, word 0 (32-bit IEEE754). /// B3:0/0 — bit file 3, word 0, bit 0. /// ST9:0 — string file 9, string 0 (82-byte fixed-length + length word). /// T4:0.ACC — timer file 4, timer 0, accumulator sub-element. /// C5:0.PRE — counter file 5, counter 0, preset sub-element. /// I:0/0 — input file, slot 0, bit 0 (no file-number for I/O). /// O:1/2 — output file, slot 1, bit 2. /// S:1 — status file, word 1. /// L9:0 — long-integer file (SLC 5/05+, 32-bit). /// /// Pass the original string straight through to libplctag's name=... attribute — /// the PLC-side decoder handles the format. This parser only validates the shape + surfaces /// the structural pieces for driver-side routing (e.g. deciding whether a tag needs /// bit-level read-modify-write). /// public sealed record AbLegacyAddress( string FileLetter, int? FileNumber, int WordNumber, int? BitIndex, string? SubElement) { public string ToLibplctagName() { var file = FileNumber is null ? FileLetter : $"{FileLetter}{FileNumber}"; var wordPart = $"{file}:{WordNumber}"; if (SubElement is not null) wordPart += $".{SubElement}"; if (BitIndex is not null) wordPart += $"/{BitIndex}"; return wordPart; } public static AbLegacyAddress? TryParse(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var src = value.Trim(); // BitIndex: trailing /N int? bitIndex = null; var slashIdx = src.IndexOf('/'); if (slashIdx >= 0) { if (!int.TryParse(src[(slashIdx + 1)..], out var bit) || bit < 0 || bit > 31) return null; bitIndex = bit; src = src[..slashIdx]; } // SubElement: trailing .NAME (ACC / PRE / EN / DN / TT / CU / CD / FD / etc.) string? subElement = null; var dotIdx = src.LastIndexOf('.'); if (dotIdx >= 0) { var candidate = src[(dotIdx + 1)..]; if (candidate.Length > 0 && candidate.All(char.IsLetter)) { subElement = candidate.ToUpperInvariant(); src = src[..dotIdx]; } } var colonIdx = src.IndexOf(':'); if (colonIdx <= 0) return null; var filePart = src[..colonIdx]; var wordPart = src[(colonIdx + 1)..]; if (!int.TryParse(wordPart, out var word) || word < 0) return null; // File letter + optional file number (single letter for I/O/S, letter+number otherwise). if (filePart.Length == 0 || !char.IsLetter(filePart[0])) return null; var letterEnd = 1; while (letterEnd < filePart.Length && char.IsLetter(filePart[letterEnd])) letterEnd++; var letter = filePart[..letterEnd].ToUpperInvariant(); int? fileNumber = null; if (letterEnd < filePart.Length) { if (!int.TryParse(filePart[letterEnd..], out var fn) || fn < 0) return null; fileNumber = fn; } // Reject unknown file letters — these cover SLC/ML/PLC-5 canonical families. if (!IsKnownFileLetter(letter)) return null; return new AbLegacyAddress(letter, fileNumber, word, bitIndex, subElement); } private static bool IsKnownFileLetter(string letter) => letter switch { "N" or "F" or "B" or "L" or "ST" or "T" or "C" or "R" or "I" or "O" or "S" or "A" => true, _ => false, }; }