@@ -32,6 +32,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, (string Host, string Field)> _overrideNodesByName =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _toolingNodesByName =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, (string Host, string Slot, string Axis)> _offsetNodesByName =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||
|
||||
/// <summary>
|
||||
@@ -69,6 +73,24 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// </summary>
|
||||
private static readonly string[] OverrideFieldNames = ["Feed", "Rapid", "Spindle", "Jog"];
|
||||
|
||||
/// <summary>
|
||||
/// Names of the standard work-coordinate offset slots surfaced under
|
||||
/// <c>Offsets/</c> per device — G54..G59 from <c>cnc_rdzofs(n=1..6)</c>
|
||||
/// (issue #260). Extended G54.1 P1..P48 surfaces are deferred to a follow-up
|
||||
/// PR because <c>cnc_rdzofsr</c> uses a different range surface.
|
||||
/// </summary>
|
||||
private static readonly string[] WorkOffsetSlotNames =
|
||||
[
|
||||
"G54", "G55", "G56", "G57", "G58", "G59",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Axis columns surfaced under each <c>Offsets/{slot}/</c> folder. Per the F1-d
|
||||
/// plan a fixed 3-axis (X/Y/Z) view is used; lathes / mills with extra rotational
|
||||
/// offsets get those columns exposed as 0.0 until a follow-up extends the surface.
|
||||
/// </summary>
|
||||
private static readonly string[] WorkOffsetAxisNames = ["X", "Y", "Z"];
|
||||
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
|
||||
|
||||
@@ -141,6 +163,28 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
(device.Options.HostAddress, field);
|
||||
}
|
||||
}
|
||||
|
||||
// Tooling/CurrentTool — single Int16 node per device (issue #260). Tool
|
||||
// life + active offset index are deferred per the F1-d plan; they need
|
||||
// ODBTLIFE* unions whose shape varies per series.
|
||||
if (FocasCapabilityMatrix.SupportsTooling(device.Options.Series))
|
||||
{
|
||||
_toolingNodesByName[ToolingReferenceFor(device.Options.HostAddress, "CurrentTool")] =
|
||||
device.Options.HostAddress;
|
||||
}
|
||||
|
||||
// Offsets/{G54..G59}/{X|Y|Z} — fixed 3-axis view of the standard work-
|
||||
// coordinate offsets (issue #260). Capability matrix gates by series so
|
||||
// legacy CNCs that don't support cnc_rdzofs don't produce the subtree.
|
||||
if (FocasCapabilityMatrix.SupportsWorkOffsets(device.Options.Series))
|
||||
{
|
||||
foreach (var slot in WorkOffsetSlotNames)
|
||||
foreach (var axis in WorkOffsetAxisNames)
|
||||
{
|
||||
_offsetNodesByName[OffsetReferenceFor(device.Options.HostAddress, slot, axis)] =
|
||||
(device.Options.HostAddress, slot, axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_options.Probe.Enabled)
|
||||
@@ -184,6 +228,8 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
_productionNodesByName.Clear();
|
||||
_modalNodesByName.Clear();
|
||||
_overrideNodesByName.Clear();
|
||||
_toolingNodesByName.Clear();
|
||||
_offsetNodesByName.Clear();
|
||||
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
||||
}
|
||||
|
||||
@@ -237,6 +283,22 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fixed-tree Tooling/CurrentTool — served from cached cnc_rdtnum snapshot
|
||||
// refreshed on the probe tick (issue #260). No wire call here.
|
||||
if (_toolingNodesByName.TryGetValue(reference, out var toolingHost))
|
||||
{
|
||||
results[i] = ReadToolingField(toolingHost, "CurrentTool", now);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fixed-tree Offsets/{slot}/{axis} — served from cached cnc_rdzofs(1..6)
|
||||
// snapshot refreshed on the probe tick (issue #260). No wire call here.
|
||||
if (_offsetNodesByName.TryGetValue(reference, out var offsetKey))
|
||||
{
|
||||
results[i] = ReadOffsetField(offsetKey.Host, offsetKey.Slot, offsetKey.Axis, now);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_tagsByName.TryGetValue(reference, out var def))
|
||||
{
|
||||
results[i] = new DataValueSnapshot(null, FocasStatusMapper.BadNodeIdUnknown, null, now);
|
||||
@@ -435,6 +497,50 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
WriteIdempotent: false));
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed-tree Tooling/ subfolder — single Int16 CurrentTool node from
|
||||
// cnc_rdtnum (issue #260). Tool life + active offset index are deferred
|
||||
// per the F1-d plan because the FWLIB ODBTLIFE* unions vary per series.
|
||||
if (FocasCapabilityMatrix.SupportsTooling(device.Series))
|
||||
{
|
||||
var toolingFolder = deviceFolder.Folder("Tooling", "Tooling");
|
||||
var toolingRef = ToolingReferenceFor(device.HostAddress, "CurrentTool");
|
||||
toolingFolder.Variable("CurrentTool", "CurrentTool", new DriverAttributeInfo(
|
||||
FullName: toolingRef,
|
||||
DriverDataType: DriverDataType.Int16,
|
||||
IsArray: false,
|
||||
ArrayDim: null,
|
||||
SecurityClass: SecurityClassification.ViewOnly,
|
||||
IsHistorized: false,
|
||||
IsAlarm: false,
|
||||
WriteIdempotent: false));
|
||||
}
|
||||
|
||||
// Fixed-tree Offsets/ subfolder — G54..G59 each with X/Y/Z Float64 axes
|
||||
// from cnc_rdzofs(n=1..6) (issue #260). Capability matrix gates the surface
|
||||
// by series so legacy controllers without cnc_rdzofs support don't expose
|
||||
// dead nodes. Extended G54.1 P1..P48 surfaces are deferred to a follow-up.
|
||||
if (FocasCapabilityMatrix.SupportsWorkOffsets(device.Series))
|
||||
{
|
||||
var offsetsFolder = deviceFolder.Folder("Offsets", "Offsets");
|
||||
foreach (var slot in WorkOffsetSlotNames)
|
||||
{
|
||||
var slotFolder = offsetsFolder.Folder(slot, slot);
|
||||
foreach (var axis in WorkOffsetAxisNames)
|
||||
{
|
||||
var fullRef = OffsetReferenceFor(device.HostAddress, slot, axis);
|
||||
slotFolder.Variable(axis, axis, new DriverAttributeInfo(
|
||||
FullName: fullRef,
|
||||
DriverDataType: DriverDataType.Float64,
|
||||
IsArray: false,
|
||||
ArrayDim: null,
|
||||
SecurityClass: SecurityClassification.ViewOnly,
|
||||
IsHistorized: false,
|
||||
IsAlarm: false,
|
||||
WriteIdempotent: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -451,6 +557,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private static string OverrideReferenceFor(string hostAddress, string field) =>
|
||||
$"{hostAddress}::Override/{field}";
|
||||
|
||||
private static string ToolingReferenceFor(string hostAddress, string field) =>
|
||||
$"{hostAddress}::Tooling/{field}";
|
||||
|
||||
private static string OffsetReferenceFor(string hostAddress, string slot, string axis) =>
|
||||
$"{hostAddress}::Offsets/{slot}/{axis}";
|
||||
|
||||
private static ushort? OverrideParamFor(FocasOverrideParameters p, string field) => field switch
|
||||
{
|
||||
"Feed" => p.FeedParam,
|
||||
@@ -566,6 +678,28 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
state.LastOverrideUtc = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
// Tooling/CurrentTool + Offsets/{G54..G59}/{X|Y|Z} — same best-
|
||||
// effort policy as the other fixed-tree caches (issue #260). A
|
||||
// null result leaves the previous good snapshot in place so reads
|
||||
// keep serving until the next successful refresh.
|
||||
if (FocasCapabilityMatrix.SupportsTooling(state.Options.Series))
|
||||
{
|
||||
var tooling = await client.GetToolingAsync(ct).ConfigureAwait(false);
|
||||
if (tooling is not null)
|
||||
{
|
||||
state.LastTooling = tooling;
|
||||
state.LastToolingUtc = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
if (FocasCapabilityMatrix.SupportsWorkOffsets(state.Options.Series))
|
||||
{
|
||||
var offsets = await client.GetWorkOffsetsAsync(ct).ConfigureAwait(false);
|
||||
if (offsets is not null)
|
||||
{
|
||||
state.LastWorkOffsets = offsets;
|
||||
state.LastWorkOffsetsUtc = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
|
||||
@@ -630,6 +764,43 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
device.LastOverrideUtc, now);
|
||||
}
|
||||
|
||||
private DataValueSnapshot ReadToolingField(string hostAddress, string field, DateTime now)
|
||||
{
|
||||
if (!_devices.TryGetValue(hostAddress, out var device))
|
||||
return new DataValueSnapshot(null, FocasStatusMapper.BadNodeIdUnknown, null, now);
|
||||
if (device.LastTooling is not { } snap)
|
||||
return new DataValueSnapshot(null, FocasStatusMapper.BadCommunicationError, null, now);
|
||||
return field switch
|
||||
{
|
||||
"CurrentTool" => new DataValueSnapshot(snap.CurrentTool, FocasStatusMapper.Good,
|
||||
device.LastToolingUtc, now),
|
||||
_ => new DataValueSnapshot(null, FocasStatusMapper.BadNodeIdUnknown, null, now),
|
||||
};
|
||||
}
|
||||
|
||||
private DataValueSnapshot ReadOffsetField(string hostAddress, string slot, string axis, DateTime now)
|
||||
{
|
||||
if (!_devices.TryGetValue(hostAddress, out var device))
|
||||
return new DataValueSnapshot(null, FocasStatusMapper.BadNodeIdUnknown, null, now);
|
||||
if (device.LastWorkOffsets is not { } snap)
|
||||
return new DataValueSnapshot(null, FocasStatusMapper.BadCommunicationError, null, now);
|
||||
var match = snap.Offsets.FirstOrDefault(o =>
|
||||
string.Equals(o.Name, slot, StringComparison.OrdinalIgnoreCase));
|
||||
if (match is null)
|
||||
return new DataValueSnapshot(null, FocasStatusMapper.BadNodeIdUnknown, null, now);
|
||||
var value = axis switch
|
||||
{
|
||||
"X" => (double?)match.X,
|
||||
"Y" => match.Y,
|
||||
"Z" => match.Z,
|
||||
_ => null,
|
||||
};
|
||||
if (value is null)
|
||||
return new DataValueSnapshot(null, FocasStatusMapper.BadNodeIdUnknown, null, now);
|
||||
return new DataValueSnapshot(value.Value, FocasStatusMapper.Good,
|
||||
device.LastWorkOffsetsUtc, now);
|
||||
}
|
||||
|
||||
private void TransitionDeviceState(DeviceState state, HostState newState)
|
||||
{
|
||||
HostState old;
|
||||
@@ -717,6 +888,23 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
public FocasOverrideInfo? LastOverride { get; set; }
|
||||
public DateTime LastOverrideUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached <c>cnc_rdtnum</c> snapshot — current tool number — refreshed on
|
||||
/// every probe tick. Reads of <c>Tooling/CurrentTool</c> serve from this
|
||||
/// cache so they don't pile extra wire traffic on top of user-driven
|
||||
/// reads (issue #260).
|
||||
/// </summary>
|
||||
public FocasToolingInfo? LastTooling { get; set; }
|
||||
public DateTime LastToolingUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached <c>cnc_rdzofs(1..6)</c> snapshot — G54..G59 work-coordinate
|
||||
/// offsets — refreshed on every probe tick. Reads of
|
||||
/// <c>Offsets/{slot}/{X|Y|Z}</c> serve from this cache (issue #260).
|
||||
/// </summary>
|
||||
public FocasWorkOffsetsInfo? LastWorkOffsets { get; set; }
|
||||
public DateTime LastWorkOffsetsUtc { get; set; }
|
||||
|
||||
public void DisposeClient()
|
||||
{
|
||||
Client?.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user