namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS; /// /// Parsed FOCAS address covering the three addressing spaces a driver touches: /// (letter + byte + optional bit — X0.0, R100, /// F20.3), (CNC parameter number — /// PARAM:1020, PARAM:1815/0 for bit 0), and /// (macro variable number — MACRO:100, MACRO:500). /// /// /// PMC letters: X/Y (IO), F/G (signals between PMC + CNC), R (internal /// relay), D (data table), C (counter), K (keep relay), A /// (message display), E (extended relay), T (timer). Byte numbering is 0-based; /// bit index when present is 0–7 and uses .N for PMC or /N for parameters. /// public sealed record FocasAddress( FocasAreaKind Kind, string? PmcLetter, int Number, int? BitIndex) { public string Canonical => Kind switch { FocasAreaKind.Pmc => BitIndex is null ? $"{PmcLetter}{Number}" : $"{PmcLetter}{Number}.{BitIndex}", FocasAreaKind.Parameter => BitIndex is null ? $"PARAM:{Number}" : $"PARAM:{Number}/{BitIndex}", FocasAreaKind.Macro => $"MACRO:{Number}", _ => $"?{Number}", }; public static FocasAddress? TryParse(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var src = value.Trim(); if (src.StartsWith("PARAM:", StringComparison.OrdinalIgnoreCase)) return ParseScoped(src["PARAM:".Length..], FocasAreaKind.Parameter, bitSeparator: '/'); if (src.StartsWith("MACRO:", StringComparison.OrdinalIgnoreCase)) return ParseScoped(src["MACRO:".Length..], FocasAreaKind.Macro, bitSeparator: null); // PMC path: letter + digits + optional .bit if (src.Length < 2 || !char.IsLetter(src[0])) return null; var letter = src[0..1].ToUpperInvariant(); if (!IsValidPmcLetter(letter)) return null; var remainder = src[1..]; int? bit = null; var dotIdx = remainder.IndexOf('.'); if (dotIdx >= 0) { if (!int.TryParse(remainder[(dotIdx + 1)..], out var bitValue) || bitValue is < 0 or > 7) return null; bit = bitValue; remainder = remainder[..dotIdx]; } if (!int.TryParse(remainder, out var number) || number < 0) return null; return new FocasAddress(FocasAreaKind.Pmc, letter, number, bit); } private static FocasAddress? ParseScoped(string body, FocasAreaKind kind, char? bitSeparator) { int? bit = null; if (bitSeparator is char sep) { var slashIdx = body.IndexOf(sep); if (slashIdx >= 0) { if (!int.TryParse(body[(slashIdx + 1)..], out var bitValue) || bitValue is < 0 or > 31) return null; bit = bitValue; body = body[..slashIdx]; } } if (!int.TryParse(body, out var number) || number < 0) return null; return new FocasAddress(kind, PmcLetter: null, number, bit); } private static bool IsValidPmcLetter(string letter) => letter switch { "X" or "Y" or "F" or "G" or "R" or "D" or "C" or "K" or "A" or "E" or "T" => true, _ => false, }; } /// Addressing-space kinds the driver understands. public enum FocasAreaKind { Pmc, Parameter, Macro, }