namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS; /// /// Parsed FOCAS address covering the four 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), /// (macro variable number — MACRO:100, MACRO:500), and /// (CNC diagnostic number, optionally per-axis — /// DIAG:1031, DIAG:280/2) routed through cnc_rddiag. /// /// /// 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. /// Diagnostic addresses reuse the /N form to encode an axis index — BitIndex /// carries the 1-based axis number (0 = whole-CNC diagnostic). /// /// Multi-path / multi-channel CNCs (e.g. lathe + sub-spindle, dual-turret) expose multiple /// "paths"; selects which one a given address is read from. Encoded /// as a trailing @N after the address body but before any bit / axis suffix — /// R100@2, PARAM:1815@2, PARAM:1815@2/0, MACRO:500@3, /// DIAG:280@2/1. Defaults to 1 for back-compat (single-path CNCs). /// /// public sealed record FocasAddress( FocasAreaKind Kind, string? PmcLetter, int Number, int? BitIndex, int PathId = 1) { public string Canonical { get { var pathSuffix = PathId == 1 ? string.Empty : $"@{PathId}"; return Kind switch { FocasAreaKind.Pmc => BitIndex is null ? $"{PmcLetter}{Number}{pathSuffix}" : $"{PmcLetter}{Number}{pathSuffix}.{BitIndex}", FocasAreaKind.Parameter => BitIndex is null ? $"PARAM:{Number}{pathSuffix}" : $"PARAM:{Number}{pathSuffix}/{BitIndex}", FocasAreaKind.Macro => $"MACRO:{Number}{pathSuffix}", FocasAreaKind.Diagnostic => BitIndex is null or 0 ? $"DIAG:{Number}{pathSuffix}" : $"DIAG:{Number}{pathSuffix}/{BitIndex}", _ => $"?{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); if (src.StartsWith("DIAG:", StringComparison.OrdinalIgnoreCase)) return ParseScoped(src["DIAG:".Length..], FocasAreaKind.Diagnostic, bitSeparator: '/'); // PMC path: letter + digits + optional @path + 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]; } var pmcPath = 1; var atIdx = remainder.IndexOf('@'); if (atIdx >= 0) { if (!TryParsePathId(remainder[(atIdx + 1)..], out pmcPath)) return null; remainder = remainder[..atIdx]; } if (!int.TryParse(remainder, out var number) || number < 0) return null; return new FocasAddress(FocasAreaKind.Pmc, letter, number, bit, pmcPath); } 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]; } } // Path suffix (@N) sits between the body number and any bit/axis (which has already // been peeled off above): PARAM:1815@2/0 → body="1815@2", bit=0. var path = 1; var atIdx = body.IndexOf('@'); if (atIdx >= 0) { if (!TryParsePathId(body[(atIdx + 1)..], out path)) return null; body = body[..atIdx]; } if (!int.TryParse(body, out var number) || number < 0) return null; return new FocasAddress(kind, PmcLetter: null, number, bit, path); } private static bool TryParsePathId(string text, out int pathId) { // Path 0 is reserved (FOCAS path numbering is 1-based); upper-bound is the FWLIB // ceiling — Fanuc spec lists 10 paths max even on the largest 30i-B configurations. if (int.TryParse(text, out var v) && v is >= 1 and <= 10) { pathId = v; return true; } pathId = 0; return false; } 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, /// /// CNC diagnostic number routed through cnc_rddiag. DIAG:nnn is a /// whole-CNC diagnostic (axis = 0); DIAG:nnn/axis is per-axis (axis is the /// 1-based FANUC axis index). Like parameters, diagnostics span Int / Float / /// Bit shapes — the driver picks the wire shape based on the configured tag's /// . /// Diagnostic, }