Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasAddress.cs
Joseph Doherty 2f3eeecd17 Auto: focas-f2b — multi-path/multi-channel CNC
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
2026-04-25 19:42:58 -04:00

158 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 07 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,
}