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,
}