fix(focas): a missing O-number or refused program sub-read goes Bad, not a Good 0

WireFocasClient.GetProgramInfoAsync defaulted every field the CNC did not report:
ONumber ?? 0 when the cnc_exeprgname2 payload was too short to carry it, and
Name '' / BlockCount 0 / Mode 0 when the sub-read was refused. 0 is a genuine
O-number (a 31i reports 0 for a path-executed program -- live Z-34184), a
genuine block count, and Mode 0 is MDI, a real operating mode.

FocasProgramInfo fields are now nullable; MapProgramInfo leaves unreported
fields null and the fixed tree publishes them BadNoData while the bundle's
other fields stay Good.

Ported from the sister ignitionoee FOCAS driver (101a3b1, 2026-08-12).
This commit is contained in:
Joseph Doherty
2026-10-07 14:08:26 -04:00
parent 1dba46bd7b
commit 26b512adba
4 changed files with 135 additions and 11 deletions
@@ -1307,8 +1307,8 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
"Program/MainNumber" => state.LastProgramAxisRef?.MainProgramNumber,
"Program/Sequence" => state.LastProgramAxisRef?.SequenceNumber,
"OperationMode/Mode" => state.LastProgramInfo?.Mode,
"OperationMode/ModeText" => state.LastProgramInfo is { } pi
? FocasOpMode.ToText(pi.Mode) : null,
"OperationMode/ModeText" => state.LastProgramInfo?.Mode is { } mode
? FocasOpMode.ToText(mode) : null,
"Timers/PowerOnSeconds" => TimerValue(state, FocasTimerKind.PowerOn),
"Timers/OperatingSeconds" => TimerValue(state, FocasTimerKind.Operating),
"Timers/CuttingSeconds" => TimerValue(state, FocasTimerKind.Cutting),
@@ -1317,6 +1317,18 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
};
if (value is not null)
return new DataValueSnapshot(value, status, sourceUtc, now);
// The program bundle has been read, but the CNC did not report this field (a refused
// sub-read, or no O-number in the payload). That is Bad — never a fabricated 0, and
// not "no such node" either.
if (state.LastProgramInfo is not null
&& path is "Program/Name" or "Program/ONumber" or "Program/BlockCount"
or "OperationMode/Mode" or "OperationMode/ModeText")
return new DataValueSnapshot(
null,
status == FocasStatusMapper.Good ? FocasStatusMapper.BadNoData : status,
sourceUtc,
now);
}
return null;
}
@@ -238,12 +238,15 @@ public sealed record FocasSpindleName(string Name, string Suffix1, string Suffix
/// Program + operation-mode snapshot. Name is the currently-executing
/// program filename (e.g. "O0001.NC"); ONumber is its Fanuc O-number (1-9999).
/// Mode is the numeric code from <c>cnc_rdopmode</c> — see <see cref="FocasOpMode"/>.
/// <b>A null field means the CNC did not report it</b> (its sub-read was refused, or the
/// payload was too short to carry it); the driver publishes that node Bad. It is never
/// defaulted to <c>0</c>, which is a genuine O-number, block count, and mode (MDI).
/// </summary>
public sealed record FocasProgramInfo(
string Name,
int ONumber,
int BlockCount,
int Mode);
string? Name,
int? ONumber,
int? BlockCount,
int? Mode);
/// <summary>Human-readable text for the <see cref="FocasProgramInfo.Mode"/> integer.</summary>
public static class FocasOpMode
@@ -217,12 +217,29 @@ public sealed class WireFocasClient : IFocasClient
throw new InvalidOperationException(
$"cnc_exeprgname2 failed EW_{nameResult.Rc} and cnc_rdopmode failed EW_{modeResult.Rc}.");
var wireName = nameResult.Value;
return MapProgramInfo(nameResult, blkResult, modeResult);
}
/// <summary>
/// Combine the three program sub-reads into a <see cref="FocasProgramInfo"/>, leaving any
/// field the CNC did not report null. A refused read, or an <c>cnc_exeprgname2</c> payload
/// too short to carry the trailing O-number, used to publish a fabricated <c>0</c> at Good
/// quality — indistinguishable from a real 0 (a 31i reports O-number 0 for a program run
/// by path) and, for the mode, from MDI. Ported from the sister ignitionoee driver (101a3b1).
/// </summary>
/// <param name="name">The <c>cnc_exeprgname2</c> result.</param>
/// <param name="blockCount">The <c>cnc_rdblkcount</c> result.</param>
/// <param name="mode">The <c>cnc_rdopmode</c> result.</param>
/// <returns>The snapshot, null where the CNC did not say.</returns>
internal static FocasProgramInfo MapProgramInfo(
FocasResult<WireProgramName> name, FocasResult<int> blockCount, FocasResult<short> mode)
{
var wireName = name.IsOk ? name.Value : null;
return new FocasProgramInfo(
Name: wireName?.Name ?? string.Empty,
ONumber: wireName?.ONumber ?? 0,
BlockCount: blkResult.IsOk ? blkResult.Value : 0,
Mode: modeResult.IsOk ? modeResult.Value : 0);
Name: wireName?.Name,
ONumber: wireName?.ONumber,
BlockCount: blockCount.IsOk ? blockCount.Value : null,
Mode: mode.IsOk ? mode.Value : null);
}
/// <inheritdoc />