Auto: focas-f1c — Modal codes + overrides

Closes #259

Adds Modal/ + Override/ fixed-tree subfolders per FOCAS device, mirroring the
pattern established by Status/ (#257) and Production/ (#258): cached snapshots
refreshed on the probe tick, served from cache on read, no extra wire traffic
on top of user-driven tag reads.

Modal/ surfaces the four universally-present aux modal codes M/S/T/B from
cnc_modal(type=100..103) as Int16. **G-group decoding (groups 1..21) is deferred
to a follow-up** — the FWLIB ODBMDL union differs per series + group and the
issue body explicitly permits this scoping. Adds the cnc_modal P/Invoke +
ODBMDL struct + a generic int16 cnc_rdparam helper so the follow-up can add
G-groups without further wire-level scaffolding.

Override/ surfaces Feed/Rapid/Spindle/Jog from cnc_rdparam at MTB-specific
parameter numbers (FocasDeviceOptions.OverrideParameters; defaults to 30i:
6010/6011/6014/6015). Per-field nullable params let a deployment hide overrides
their MTB doesn't wire up; passing OverrideParameters=null suppresses the entire
Override/ subfolder for that device.

6 unit tests cover discovery shape, omitted Override folder when unconfigured,
partial Override field selection, cached-snapshot reads (Modal + Override),
BadCommunicationError before first refresh, and the FwlibFocasClient
disconnected short-circuit.
This commit is contained in:
Joseph Doherty
2026-04-25 14:26:48 -04:00
parent ae7cc15178
commit 3c2c4f29ea
6 changed files with 563 additions and 1 deletions

View File

@@ -70,6 +70,27 @@ public interface IFocasClient : IDisposable
/// </summary>
Task<FocasProductionInfo?> GetProductionAsync(CancellationToken cancellationToken)
=> Task.FromResult<FocasProductionInfo?>(null);
/// <summary>
/// Read the active modal M/S/T/B codes via <c>cnc_modal</c>. G-group decoding is
/// deferred — the FWLIB <c>ODBMDL</c> union differs per series + group and the
/// issue body permits surfacing only the universally-present M/S/T/B fields in
/// the first cut (issue #259). Returns <c>null</c> when the wire client cannot
/// supply the snapshot.
/// </summary>
Task<FocasModalInfo?> GetModalAsync(CancellationToken cancellationToken)
=> Task.FromResult<FocasModalInfo?>(null);
/// <summary>
/// Read the four operator override values (feed / rapid / spindle / jog) via
/// <c>cnc_rdparam</c>. The parameter numbers are MTB-specific so the caller passes
/// them in via <paramref name="parameters"/>; a <c>null</c> entry suppresses that
/// field's read (the corresponding node is also omitted from the address space).
/// Returns <c>null</c> when the wire client cannot supply the snapshot (issue #259).
/// </summary>
Task<FocasOverrideInfo?> GetOverrideAsync(
FocasOverrideParameters parameters, CancellationToken cancellationToken)
=> Task.FromResult<FocasOverrideInfo?>(null);
}
/// <summary>
@@ -102,6 +123,47 @@ public sealed record FocasProductionInfo(
int PartsTotal,
int CycleTimeSeconds);
/// <summary>
/// Snapshot of the active modal M/S/T/B codes (issue #259). G-group decoding is a
/// deferred follow-up — the FWLIB <c>ODBMDL</c> union differs per series + group, and
/// the issue body permits the first cut to surface only the universally-present
/// M/S/T/B fields. <c>short</c> matches the FWLIB <c>aux_data</c> width.
/// </summary>
public sealed record FocasModalInfo(
short MCode,
short SCode,
short TCode,
short BCode);
/// <summary>
/// MTB-specific FOCAS parameter numbers for the four operator overrides (issue #259).
/// Defaults match Fanuc 30i — Feed=6010, Rapid=6011, Spindle=6014, Jog=6015. A
/// <c>null</c> entry suppresses that field's read on the wire and removes the matching
/// node from the address space; this lets a deployment hide overrides their MTB doesn't
/// wire up rather than always serving Bad.
/// </summary>
public sealed record FocasOverrideParameters(
ushort? FeedParam,
ushort? RapidParam,
ushort? SpindleParam,
ushort? JogParam)
{
/// <summary>Stock 30i defaults — Feed=6010, Rapid=6011, Spindle=6014, Jog=6015.</summary>
public static FocasOverrideParameters Default { get; } = new(6010, 6011, 6014, 6015);
}
/// <summary>
/// Snapshot of the four operator overrides (issue #259). Each value is a percentage
/// surfaced as <c>Int16</c>; a value of <c>null</c> means the corresponding parameter
/// was not configured (suppressed at <see cref="FocasOverrideParameters"/>). All four
/// fields nullable so the driver can omit nodes whose MTB parameter is unset.
/// </summary>
public sealed record FocasOverrideInfo(
short? Feed,
short? Rapid,
short? Spindle,
short? Jog);
/// <summary>Factory for <see cref="IFocasClient"/>s. One client per configured device.</summary>
public interface IFocasClientFactory
{