chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
287
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs
Normal file
287
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
/// <summary>
|
||||
/// Wire-layer abstraction over one FOCAS session to a CNC. The driver holds one per
|
||||
/// configured device; lifetime matches the device.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The default implementation is <see cref="Wire.WireFocasClient"/> — a pure-managed
|
||||
/// FOCAS/2 Ethernet client that speaks the wire protocol directly on TCP:8193. No
|
||||
/// P/Invoke, no native DLLs, no out-of-process isolation.</para>
|
||||
///
|
||||
/// <para><see cref="UnimplementedFocasClientFactory"/> is a scaffolding backend that
|
||||
/// throws on <see cref="IFocasClientFactory.Create"/> — selected by
|
||||
/// <c>"Backend": "unimplemented"</c> so a DriverInstance row can be seeded before the CNC
|
||||
/// endpoint is reachable without silently reading stale data.</para>
|
||||
/// </remarks>
|
||||
public interface IFocasClient : IDisposable
|
||||
{
|
||||
/// <summary>Open the FWLIB handle + TCP session. Idempotent.</summary>
|
||||
Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>True when the FWLIB handle is valid + the socket is up.</summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Read the value at <paramref name="address"/> in the requested
|
||||
/// <paramref name="type"/>. Returns a boxed .NET value + the OPC UA status mapped
|
||||
/// through <see cref="FocasStatusMapper"/>.
|
||||
/// </summary>
|
||||
Task<(object? value, uint status)> ReadAsync(
|
||||
FocasAddress address,
|
||||
FocasDataType type,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Write <paramref name="value"/> to <paramref name="address"/>. Returns the mapped
|
||||
/// OPC UA status (0 = Good).
|
||||
/// </summary>
|
||||
Task<uint> WriteAsync(
|
||||
FocasAddress address,
|
||||
FocasDataType type,
|
||||
object? value,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Cheap health probe — e.g. <c>cnc_rdcncstat</c>. Returns <c>true</c> when the CNC
|
||||
/// responds with any valid status.
|
||||
/// </summary>
|
||||
Task<bool> ProbeAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read active alarm messages from the CNC via <c>cnc_rdalmmsg2</c>. Returns
|
||||
/// zero-or-more active alarms. Null / empty list means "no alarms currently
|
||||
/// active". IAlarmSource projection polls this at a configurable interval +
|
||||
/// emits transitions (raise / clear) on the driver's <c>OnAlarmEvent</c>.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken cancellationToken);
|
||||
|
||||
// ---- Fixed-tree T1 (identity + axis discovery + fast-poll dynamic bundle) ----
|
||||
|
||||
/// <summary>
|
||||
/// Read CNC identity via <c>cnc_sysinfo</c>. Populates the <c>Identity/*</c>
|
||||
/// subtree of the fixed-node surface. Callable once at session open; the
|
||||
/// values don't change across the session.
|
||||
/// </summary>
|
||||
Task<FocasSysInfo> GetSysInfoAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read the CNC's configured axis names via <c>cnc_rdaxisname</c>. The driver
|
||||
/// uses these to build the <c>Axes/{name}/</c> subtree and to index
|
||||
/// <see cref="ReadDynamicAsync"/> calls.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read the CNC's configured spindle names via <c>cnc_rdspdlname</c>. Drives
|
||||
/// the <c>Spindle/{name}/</c> subtree.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<FocasSpindleName>> GetSpindleNamesAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read the fast-poll dynamic bundle for one axis via <c>cnc_rddynamic2</c>.
|
||||
/// Returns the current position quadruple (absolute / machine / relative /
|
||||
/// distance-to-go) plus actual feed rate + actual spindle speed + alarm
|
||||
/// flags + program / sequence numbers — one network round-trip per call.
|
||||
/// </summary>
|
||||
Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken);
|
||||
|
||||
// ---- Fixed-tree T2 (program + operation mode) ----
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate program + operation-mode snapshot. One wire round-trip per
|
||||
/// underlying FWLIB call — <c>cnc_rdblkcount</c>, <c>cnc_exeprgname2</c>,
|
||||
/// <c>cnc_rdopmode</c>. The driver polls this on a slower cadence than
|
||||
/// <see cref="ReadDynamicAsync"/> since program / mode transitions happen
|
||||
/// on human-operator timescales.
|
||||
/// </summary>
|
||||
Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken);
|
||||
|
||||
// ---- Fixed-tree T3 (timers) ----
|
||||
|
||||
/// <summary>
|
||||
/// Read one CNC cumulative timer. Kind selects PowerOn / Operating / Cutting /
|
||||
/// Cycle. Values are seconds — the managed side already converted the native
|
||||
/// minute+msec representation so downstream nodes display uniform units.
|
||||
/// </summary>
|
||||
Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken);
|
||||
|
||||
// ---- Fixed-tree T3.5 (servo meters) ----
|
||||
|
||||
/// <summary>
|
||||
/// Read the servo-load meter percentages across all configured axes.
|
||||
/// Values are percentages (scaled by <c>10^Dec</c>). Empty list on a
|
||||
/// disconnected session or unsupported CNC.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken cancellationToken);
|
||||
|
||||
// ---- Fixed-tree T3.6 (spindle meters) ----
|
||||
|
||||
/// <summary>
|
||||
/// Read per-spindle load percentages. Result list index corresponds to
|
||||
/// spindle index from <see cref="GetSpindleNamesAsync"/>. Empty list on a
|
||||
/// disconnected session or when the CNC doesn't support the call (older
|
||||
/// series like 16i may return EW_FUNC).
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read per-spindle maximum RPM values. Static configuration, fetched once at
|
||||
/// bootstrap. Index alignment as per <see cref="GetSpindleLoadsAsync"/>.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>One servo-meter entry — one axis's current load percentage.</summary>
|
||||
public sealed record FocasServoLoad(string AxisName, double LoadPercent);
|
||||
|
||||
/// <summary>Which cumulative counter <see cref="IFocasClient.GetTimerAsync"/> reads.</summary>
|
||||
public enum FocasTimerKind
|
||||
{
|
||||
/// <summary>Machine power-on hours — resets never.</summary>
|
||||
PowerOn = 0,
|
||||
/// <summary>Cycle operating time — resets when the operator clears the counter.</summary>
|
||||
Operating = 1,
|
||||
/// <summary>Cutting time — only counts while in cutting feed.</summary>
|
||||
Cutting = 2,
|
||||
/// <summary>Cycle time since the last program start.</summary>
|
||||
Cycle = 3,
|
||||
}
|
||||
|
||||
/// <summary>One cumulative timer reading. <see cref="TotalSeconds"/> is the canonical unit.</summary>
|
||||
public sealed record FocasTimer(FocasTimerKind Kind, int Minutes, int Milliseconds)
|
||||
{
|
||||
/// <summary>Cumulative time in seconds — <c>Minutes * 60 + Milliseconds / 1000</c>.</summary>
|
||||
public double TotalSeconds => Minutes * 60.0 + Milliseconds / 1000.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CNC identity snapshot from <c>cnc_sysinfo</c>. Strings are trimmed ASCII.
|
||||
/// </summary>
|
||||
public sealed record FocasSysInfo(
|
||||
int AddInfo,
|
||||
int MaxAxis,
|
||||
string CncType, // "M" (mill) / "T" (lathe)
|
||||
string MtType,
|
||||
string Series, // e.g. "30i"
|
||||
string Version, // e.g. "A1.0"
|
||||
int AxesCount);
|
||||
|
||||
/// <summary>One configured axis name (e.g. "X", "X1").</summary>
|
||||
public sealed record FocasAxisName(string Name, string Suffix)
|
||||
{
|
||||
/// <summary>
|
||||
/// Display name — name + suffix concatenated, trimmed. Empty suffix yields
|
||||
/// just the name (the common case on single-channel CNCs).
|
||||
/// </summary>
|
||||
public string Display => string.IsNullOrEmpty(Suffix) ? Name : $"{Name}{Suffix}";
|
||||
}
|
||||
|
||||
/// <summary>One configured spindle name (e.g. "S1").</summary>
|
||||
public sealed record FocasSpindleName(string Name, string Suffix1, string Suffix2, string Suffix3)
|
||||
{
|
||||
public string Display
|
||||
{
|
||||
get
|
||||
{
|
||||
var s = Name + Suffix1 + Suffix2 + Suffix3;
|
||||
return s.TrimEnd('\0', ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fast-poll bundle for one axis. Position values are scaled integers; the caller
|
||||
/// divides by <c>10^DecimalPlaces</c> to get the decimal value. DecimalPlaces is
|
||||
/// currently left to the caller to supply (via device config or a future
|
||||
/// <c>cnc_getfigure</c> path once that export lands).
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 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"/>.
|
||||
/// </summary>
|
||||
public sealed record FocasProgramInfo(
|
||||
string Name,
|
||||
int ONumber,
|
||||
int BlockCount,
|
||||
int Mode);
|
||||
|
||||
/// <summary>Human-readable text for the <see cref="FocasProgramInfo.Mode"/> integer.</summary>
|
||||
public static class FocasOpMode
|
||||
{
|
||||
public static string ToText(int mode) => mode switch
|
||||
{
|
||||
0 => "MDI",
|
||||
1 => "AUTO",
|
||||
2 => "TJOG",
|
||||
3 => "EDIT",
|
||||
4 => "HANDLE",
|
||||
5 => "JOG",
|
||||
6 => "TEACH_IN_HANDLE",
|
||||
7 => "REFERENCE",
|
||||
8 => "REMOTE",
|
||||
9 => "TEST",
|
||||
_ => $"Mode{mode}",
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record FocasDynamicSnapshot(
|
||||
int AxisIndex,
|
||||
int AlarmFlags,
|
||||
int ProgramNumber,
|
||||
int MainProgramNumber,
|
||||
int SequenceNumber,
|
||||
int ActualFeedRate,
|
||||
int ActualSpindleSpeed,
|
||||
int AbsolutePosition,
|
||||
int MachinePosition,
|
||||
int RelativePosition,
|
||||
int DistanceToGo);
|
||||
|
||||
/// <summary>
|
||||
/// One active alarm surfaced by <see cref="IFocasClient.ReadAlarmsAsync"/>. Shape
|
||||
/// mirrors <c>ODBALMMSG2</c> but normalises the message bytes to a .NET string.
|
||||
/// </summary>
|
||||
public sealed record FocasActiveAlarm(
|
||||
int AlarmNumber,
|
||||
short Type,
|
||||
short Axis,
|
||||
string Message);
|
||||
|
||||
/// <summary>Factory for <see cref="IFocasClient"/>s. One client per configured device.</summary>
|
||||
public interface IFocasClientFactory
|
||||
{
|
||||
IFocasClient Create();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scaffolding factory — throws on <see cref="Create"/> so a DriverInstance row can be
|
||||
/// seeded ahead of the CNC endpoint being reachable without silently reading stale data.
|
||||
/// Select via <c>"Backend": "unimplemented"</c> in driver config. Flip to
|
||||
/// <c>"Backend": "wire"</c> once the CNC is provisioned.
|
||||
/// </summary>
|
||||
public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
|
||||
{
|
||||
public IFocasClient Create() => throw new NotSupportedException(
|
||||
"FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " +
|
||||
"once the CNC is provisioned — see docs/drivers/FOCAS.md.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Well-known FOCAS alarm types from <c>fwlib32.h</c> <c>ALM_TYPE_*</c>. Narrow subset —
|
||||
/// the full list is ~15 types per model; these cover the universally-present categories.
|
||||
/// </summary>
|
||||
public static class FocasAlarmType
|
||||
{
|
||||
/// <summary>Pass to <see cref="IFocasClient.ReadAlarmsAsync"/>-equivalent to mean "any type".</summary>
|
||||
public const int All = -1;
|
||||
public const int Parameter = 0; // ALM_P
|
||||
public const int PulseCode = 1; // ALM_Y (servo)
|
||||
public const int Overtravel = 2; // ALM_O
|
||||
public const int Overheat = 3; // ALM_H
|
||||
public const int Servo = 4; // ALM_S
|
||||
public const int DataIo = 5; // ALM_T
|
||||
public const int MemoryCheck = 6; // ALM_M
|
||||
public const int MacroAlarm = 13; // ALM_MC — used by #3006 etc.
|
||||
}
|
||||
Reference in New Issue
Block a user