feat(focas): cnc_statinfo status flags as Status/ fixed-tree nodes (#257)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -751,6 +751,15 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
EmitFixedVariable(opMode, "OperationMode", "ModeText", DriverDataType.String);
|
||||
}
|
||||
|
||||
// Fixed-tree T2.5 — Status subtree (cnc_statinfo flags, #257), gated on its own probe.
|
||||
if (cache.Capabilities.Status)
|
||||
{
|
||||
var status = deviceFolder.Folder("Status", "Status");
|
||||
foreach (var field in StatusFields)
|
||||
EmitFixedVariable(status, "Status", field, DriverDataType.Int32);
|
||||
EmitFixedVariable(status, "Status", "RunText", DriverDataType.String);
|
||||
}
|
||||
|
||||
// Fixed-tree T3 — Timers subtree (power-on / operating / cutting / cycle).
|
||||
if (cache.Capabilities.Timers)
|
||||
{
|
||||
@@ -1008,6 +1017,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var servoLoads = await SafeProbe(() => client.GetServoLoadsAsync(ct), []);
|
||||
var programInfo = await SafeTryProbe(() => client.GetProgramInfoAsync(ct));
|
||||
var timer = await SafeTryProbe(() => client.GetTimerAsync(FocasTimerKind.PowerOn, ct));
|
||||
var statusProbe = await SafeTryProbe(() => client.GetStatusAsync(ct));
|
||||
var spindleLoad = await SafeProbe(() => client.GetSpindleLoadsAsync(ct), []);
|
||||
|
||||
var caps = new FocasFixedTreeCapabilities(
|
||||
@@ -1016,7 +1026,9 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
SpindleMotorSpeed: spindleMotorSpeeds.Count > 0,
|
||||
ServoLoad: servoLoads.Count > 0,
|
||||
ProgramInfo: programInfo is not null,
|
||||
Timers: timer is not null);
|
||||
Timers: timer is not null,
|
||||
Status: statusProbe is not null);
|
||||
state.LastStatus = statusProbe;
|
||||
|
||||
state.FixedTreeCache = new FocasFixedTreeCache(sys, [.. axes], [.. spindles], caps);
|
||||
// The bootstrap IS an answer from the device, and Identity comes entirely from it —
|
||||
@@ -1054,6 +1066,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
var programPollDue = DateTime.MinValue;
|
||||
var timerPollDue = DateTime.MinValue;
|
||||
var statusPollDue = DateTime.MinValue;
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
@@ -1129,6 +1142,20 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
programPollDue = DateTime.UtcNow + programInterval;
|
||||
}
|
||||
|
||||
// Status flags (cnc_statinfo, #257) — same cadence as program/mode: run/emergency/alarm
|
||||
// transitions are operator-timescale events and one call returns all of them.
|
||||
if (cache.Capabilities.Status
|
||||
&& programInterval > TimeSpan.Zero && DateTime.UtcNow >= statusPollDue)
|
||||
{
|
||||
try { state.LastStatus = await client.GetStatusAsync(ct).ConfigureAwait(false); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* transient — keep the last value; staleness is dated by LastFixedPollUtc */
|
||||
_logger.LogDebug(ex, "FOCAS status poll failed for {Host}", state.Options.HostAddress);
|
||||
}
|
||||
statusPollDue = DateTime.UtcNow + programInterval;
|
||||
}
|
||||
|
||||
// Timers — slowest cadence. Fires 4 FWLIB calls per tick (one per kind).
|
||||
var timerInterval = _options.FixedTree.TimerPollInterval;
|
||||
if (cache.Capabilities.Timers
|
||||
@@ -1245,6 +1272,8 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string[] StatusFields = ["Auto", "Run", "Motion", "Mstb", "Emergency", "Alarm", "Edit", "TmMode"];
|
||||
|
||||
private static object? TimerValue(DeviceState state, FocasTimerKind kind) =>
|
||||
state.LastTimers.TryGetValue(kind, out var t) ? (object)t.TotalSeconds : null;
|
||||
|
||||
@@ -1385,6 +1414,15 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
"OperationMode/Mode" => state.LastProgramInfo?.Mode,
|
||||
"OperationMode/ModeText" => state.LastProgramInfo?.Mode is { } mode
|
||||
? FocasOpMode.ToText(mode) : null,
|
||||
"Status/Auto" => state.LastStatus?.Auto,
|
||||
"Status/Run" => state.LastStatus?.Run,
|
||||
"Status/Motion" => state.LastStatus?.Motion,
|
||||
"Status/Mstb" => state.LastStatus?.Mstb,
|
||||
"Status/Emergency" => state.LastStatus?.Emergency,
|
||||
"Status/Alarm" => state.LastStatus?.Alarm,
|
||||
"Status/Edit" => state.LastStatus?.Edit,
|
||||
"Status/TmMode" => state.LastStatus?.TmMode,
|
||||
"Status/RunText" => state.LastStatus is { } st ? FocasRunState.ToText(st.Run) : null,
|
||||
"Timers/PowerOnSeconds" => TimerValue(state, FocasTimerKind.PowerOn),
|
||||
"Timers/OperatingSeconds" => TimerValue(state, FocasTimerKind.Operating),
|
||||
"Timers/CuttingSeconds" => TimerValue(state, FocasTimerKind.Cutting),
|
||||
@@ -1618,7 +1656,8 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
bool SpindleMotorSpeed, // cnc_rdspmeter type 1 (motor speed) bootstrap probe succeeded
|
||||
bool ServoLoad, // cnc_rdsvmeter bootstrap probe returned data
|
||||
bool ProgramInfo, // cnc_exeprgname2 + cnc_rdblkcount + cnc_rdopmode work
|
||||
bool Timers); // cnc_rdtimer works for at least PowerOn
|
||||
bool Timers, // cnc_rdtimer works for at least PowerOn
|
||||
bool Status = false); // cnc_statinfo answered at bootstrap (#257)
|
||||
|
||||
internal sealed class DeviceState(FocasHostAddress parsedAddress, FocasDeviceOptions options)
|
||||
{
|
||||
@@ -1683,6 +1722,8 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// holding no value.
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<string, double> LastFixedSnapshots { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>Gets or sets the last <c>cnc_statinfo</c> snapshot (#257); null until the first answer.</summary>
|
||||
public FocasStatus? LastStatus { get; set; }
|
||||
/// <summary>Gets or sets the last program information snapshot.</summary>
|
||||
public FocasProgramInfo? LastProgramInfo { get; set; }
|
||||
/// <summary>Gets or sets the cached first-axis dynamic snapshot — feeds Program/Number, /MainNumber, /Sequence.</summary>
|
||||
|
||||
@@ -125,6 +125,17 @@ public interface IFocasClient : IDisposable
|
||||
/// <returns>The aggregate program + operation-mode snapshot.</returns>
|
||||
Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken);
|
||||
|
||||
// ---- Fixed-tree T2.5 (CNC status flags, Gitea #257) ----
|
||||
|
||||
/// <summary>
|
||||
/// Read the CNC status flags via <c>cnc_statinfo</c> (<c>ODBST</c>). Populates the
|
||||
/// <c>Status/*</c> subtree. Throws when the CNC refuses the call, which the driver's
|
||||
/// bootstrap probe reads as "this series has no status block" and suppresses the subtree.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The CNC status flag snapshot.</returns>
|
||||
Task<FocasStatus> GetStatusAsync(CancellationToken cancellationToken);
|
||||
|
||||
// ---- Fixed-tree T3 (timers) ----
|
||||
|
||||
/// <summary>
|
||||
@@ -172,6 +183,38 @@ public interface IFocasClient : IDisposable
|
||||
Task<IReadOnlyList<double?>> GetSpindleMotorSpeedsAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CNC status flags from <c>cnc_statinfo</c> (<c>ODBST</c>) — raw FANUC codes, published verbatim as
|
||||
/// Int32 so a client sees exactly what the control reported. <see cref="Run"/> additionally has a
|
||||
/// text projection (<see cref="FocasRunState.ToText"/>).
|
||||
/// </summary>
|
||||
/// <param name="Auto">Automatic-operation mode code.</param>
|
||||
/// <param name="Run">Automatic-operation state (0 RESET, 1 STOP, 2 HOLD, 3 START, 4 MSTR).</param>
|
||||
/// <param name="Motion">Axis motion (0 none, 1 motion, 2 dwell).</param>
|
||||
/// <param name="Mstb">M/S/T/B function state (0 none, 1 executing).</param>
|
||||
/// <param name="Emergency">Emergency stop (0 not, 1 emergency, 2 reset).</param>
|
||||
/// <param name="Alarm">Alarm state (0 none, 1 alarm, 2 battery-low, 3 fan).</param>
|
||||
/// <param name="Edit">Program-edit state.</param>
|
||||
/// <param name="TmMode">T/M mode as reported by the control.</param>
|
||||
public sealed record FocasStatus(int Auto, int Run, int Motion, int Mstb, int Emergency, int Alarm, int Edit, int TmMode);
|
||||
|
||||
/// <summary>Human-readable text for <see cref="FocasStatus.Run"/>.</summary>
|
||||
public static class FocasRunState
|
||||
{
|
||||
/// <summary>Converts the <c>ODBST.run</c> code to its operator-facing label.</summary>
|
||||
/// <param name="run">The run-state code.</param>
|
||||
/// <returns>The label, or <c>Run{n}</c> for a code outside the documented set.</returns>
|
||||
public static string ToText(int run) => run switch
|
||||
{
|
||||
0 => "RESET",
|
||||
1 => "STOP",
|
||||
2 => "HOLD",
|
||||
3 => "START",
|
||||
4 => "MSTR",
|
||||
_ => $"Run{run}",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One servo-meter entry — one axis's current load percentage; null when the CNC's record
|
||||
/// described no usable value (never a fabricated 0).
|
||||
|
||||
@@ -90,6 +90,10 @@ public sealed class SynchronizedFocasClient : IFocasClient
|
||||
public Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken) =>
|
||||
RunBoundedAsync(ct => _inner.GetProgramInfoAsync(ct), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<FocasStatus> GetStatusAsync(CancellationToken cancellationToken) =>
|
||||
RunBoundedAsync(ct => _inner.GetStatusAsync(ct), cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken) =>
|
||||
RunBoundedAsync(ct => _inner.GetTimerAsync(kind, ct), cancellationToken);
|
||||
|
||||
@@ -129,6 +129,18 @@ public sealed class WireFocasClient : IFocasClient
|
||||
AxesCount: axesCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FocasStatus> GetStatusAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
RequireConnected();
|
||||
var result = await _wire.ReadStatusAsync(cancellationToken).ConfigureAwait(false);
|
||||
ThrowIfRcNonZero(result.Rc, "cnc_statinfo", result.IsOk);
|
||||
return MapStatus(result.Value!);
|
||||
}
|
||||
|
||||
internal static FocasStatus MapStatus(WireStatus s) =>
|
||||
new(s.Auto, s.Run, s.Motion, s.Mstb, s.Emergency, s.Alarm, s.Edit, s.TmMode);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user