Adds optional `@N` path suffix to FocasAddress (PARAM:1815@2, R100@3.0, MACRO:500@2, DIAG:280@2/1) with PathId defaulting to 1 for back-compat. Per-device PathCount is discovered via cnc_rdpathnum at first connect and cached on DeviceState; reads with PathId>PathCount return BadOutOfRange. The driver issues cnc_setpath before each non-default-path read and tracks LastSetPath so repeat reads on the same path skip the wire call. Closes #264
158 lines
6.5 KiB
C#
158 lines
6.5 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||
|
||
/// <summary>
|
||
/// Parsed FOCAS address covering the four addressing spaces a driver touches:
|
||
/// <see cref="FocasAreaKind.Pmc"/> (letter + byte + optional bit — <c>X0.0</c>, <c>R100</c>,
|
||
/// <c>F20.3</c>), <see cref="FocasAreaKind.Parameter"/> (CNC parameter number —
|
||
/// <c>PARAM:1020</c>, <c>PARAM:1815/0</c> for bit 0), <see cref="FocasAreaKind.Macro"/>
|
||
/// (macro variable number — <c>MACRO:100</c>, <c>MACRO:500</c>), and
|
||
/// <see cref="FocasAreaKind.Diagnostic"/> (CNC diagnostic number, optionally per-axis —
|
||
/// <c>DIAG:1031</c>, <c>DIAG:280/2</c>) routed through <c>cnc_rddiag</c>.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// PMC letters: <c>X/Y</c> (IO), <c>F/G</c> (signals between PMC + CNC), <c>R</c> (internal
|
||
/// relay), <c>D</c> (data table), <c>C</c> (counter), <c>K</c> (keep relay), <c>A</c>
|
||
/// (message display), <c>E</c> (extended relay), <c>T</c> (timer). Byte numbering is 0-based;
|
||
/// bit index when present is 0–7 and uses <c>.N</c> for PMC or <c>/N</c> for parameters.
|
||
/// Diagnostic addresses reuse the <c>/N</c> form to encode an axis index — <c>BitIndex</c>
|
||
/// carries the 1-based axis number (0 = whole-CNC diagnostic).
|
||
/// <para>
|
||
/// Multi-path / multi-channel CNCs (e.g. lathe + sub-spindle, dual-turret) expose multiple
|
||
/// "paths"; <see cref="PathId"/> selects which one a given address is read from. Encoded
|
||
/// as a trailing <c>@N</c> after the address body but before any bit / axis suffix —
|
||
/// <c>R100@2</c>, <c>PARAM:1815@2</c>, <c>PARAM:1815@2/0</c>, <c>MACRO:500@3</c>,
|
||
/// <c>DIAG:280@2/1</c>. Defaults to <c>1</c> for back-compat (single-path CNCs).
|
||
/// </para>
|
||
/// </remarks>
|
||
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,
|
||
};
|
||
}
|
||
|
||
/// <summary>Addressing-space kinds the driver understands.</summary>
|
||
public enum FocasAreaKind
|
||
{
|
||
Pmc,
|
||
Parameter,
|
||
Macro,
|
||
/// <summary>
|
||
/// CNC diagnostic number routed through <c>cnc_rddiag</c>. <c>DIAG:nnn</c> is a
|
||
/// whole-CNC diagnostic (axis = 0); <c>DIAG:nnn/axis</c> 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
|
||
/// <see cref="FocasDataType"/>.
|
||
/// </summary>
|
||
Diagnostic,
|
||
}
|