docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -30,15 +30,6 @@ public abstract class AbCipCommandBase : DriverCommandBase
|
||||
public int TimeoutMs { get; init; } = 5000;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// The getter validates <see cref="TimeoutMs"/> (Driver.AbCip.Cli-004) — a zero or
|
||||
/// negative <c>--timeout-ms</c> would otherwise propagate as a non-positive
|
||||
/// <see cref="TimeSpan"/> into the driver. The <c>init</c> accessor is unreachable
|
||||
/// because CliFx binds <see cref="TimeoutMs"/> rather than <c>Timeout</c>; it throws
|
||||
/// <see cref="NotSupportedException"/> so an object-initializer assignment
|
||||
/// (<c>new ReadCommand { Timeout = ... }</c>) fails fast instead of being silently
|
||||
/// discarded (Driver.AbCip.Cli-006).
|
||||
/// </remarks>
|
||||
public override TimeSpan Timeout
|
||||
{
|
||||
get
|
||||
@@ -60,6 +51,7 @@ public abstract class AbCipCommandBase : DriverCommandBase
|
||||
/// probe loop would race the operator's own reads.
|
||||
/// </summary>
|
||||
/// <param name="tags">The list of tag definitions to include in the options.</param>
|
||||
/// <returns>The constructed <see cref="AbCipDriverOptions"/>.</returns>
|
||||
protected AbCipDriverOptions BuildOptions(IReadOnlyList<AbCipTagDefinition> tags) => new()
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(
|
||||
|
||||
@@ -57,8 +57,8 @@ public sealed class ProbeCommand : AbCipCommandBase
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log
|
||||
// output emitted during driver shutdown is not lost.
|
||||
// Flush Serilog before process exit so buffered log output emitted during
|
||||
// driver shutdown is not lost.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ public sealed class ReadCommand : AbCipCommandBase
|
||||
"String / Dt (default DInt). UDT / composite types are not supported here — use a full driver config.")]
|
||||
public AbCipDataType DataType { get; init; } = AbCipDataType.DInt;
|
||||
|
||||
/// <summary>Executes the read operation.</summary>
|
||||
/// <param name="console">The console for output and cancellation handling.</param>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -54,8 +52,8 @@ public sealed class ReadCommand : AbCipCommandBase
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log
|
||||
// output emitted during driver shutdown is not lost.
|
||||
// Flush Serilog before process exit so buffered log output emitted during
|
||||
// driver shutdown is not lost.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
@@ -66,6 +64,7 @@ public sealed class ReadCommand : AbCipCommandBase
|
||||
/// </summary>
|
||||
/// <param name="tagPath">The symbolic tag path.</param>
|
||||
/// <param name="type">The data type.</param>
|
||||
/// <returns>A synthetic tag name combining the tag path and data type.</returns>
|
||||
internal static string SynthesiseTagName(string tagPath, AbCipDataType type)
|
||||
=> $"{tagPath}:{type}";
|
||||
}
|
||||
|
||||
@@ -36,8 +36,6 @@ public sealed class SubscribeCommand : AbCipCommandBase
|
||||
ConfigureLogging();
|
||||
RejectStructure(DataType);
|
||||
ValidateInterval(IntervalMs);
|
||||
// Touch Timeout to surface the --timeout-ms guard (Driver.AbCip.Cli-004) before
|
||||
// we open a driver — fast-fail with a clean CommandException on bad operator input.
|
||||
_ = Timeout;
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
@@ -56,10 +54,6 @@ public sealed class SubscribeCommand : AbCipCommandBase
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
|
||||
// Driver.AbCip.Cli-003 — emit the banner BEFORE wiring OnDataChange so the
|
||||
// main-thread write cannot interleave with poll-thread change-event writes.
|
||||
// TextWriter.WriteLine is not guaranteed thread-safe; once the handler is
|
||||
// attached and SubscribeAsync starts, change events run on the poll thread.
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Subscribed to {TagPath} @ {IntervalMs}ms. Ctrl+C to stop.");
|
||||
|
||||
@@ -90,14 +84,12 @@ public sealed class SubscribeCommand : AbCipCommandBase
|
||||
catch { /* teardown best-effort */ }
|
||||
}
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log
|
||||
// lines emitted just before Ctrl+C are not lost on abrupt termination.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guards <c>--interval-ms</c> against zero or negative values (Driver.AbCip.Cli-004).
|
||||
/// Guards <c>--interval-ms</c> against zero or negative values.
|
||||
/// A non-positive interval would produce a non-positive <see cref="TimeSpan"/> into
|
||||
/// <c>SubscribeAsync</c>; the CLI should fail fast with an actionable error rather
|
||||
/// than relying on the downstream <c>PollGroupEngine</c> to clamp the value.
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed class WriteCommand : AbCipCommandBase
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log
|
||||
// Flush Serilog before process exit so buffered log
|
||||
// output emitted during driver shutdown is not lost.
|
||||
FlushLogging();
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed class ProbeCommand : AbLegacyCommandBase
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered
|
||||
// Flush Serilog before process exit so buffered
|
||||
// log output emitted during driver shutdown is not lost.
|
||||
FlushLogging();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed class ReadCommand : AbLegacyCommandBase
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered
|
||||
// Flush Serilog before process exit so buffered
|
||||
// log output emitted during driver shutdown is not lost.
|
||||
FlushLogging();
|
||||
}
|
||||
@@ -61,6 +61,7 @@ public sealed class ReadCommand : AbLegacyCommandBase
|
||||
/// <summary>Tag-name key the driver uses internally. Address+type is already unique.</summary>
|
||||
/// <param name="address">The PCCC file address.</param>
|
||||
/// <param name="type">The data type of the address.</param>
|
||||
/// <returns>The synthesised tag-name key combining <paramref name="address"/> and <paramref name="type"/>.</returns>
|
||||
internal static string SynthesiseTagName(string address, AbLegacyDataType type)
|
||||
=> $"{address}:{type}";
|
||||
}
|
||||
|
||||
@@ -93,10 +93,6 @@ public sealed class SubscribeCommand : AbLegacyCommandBase
|
||||
catch { /* teardown best-effort */ }
|
||||
}
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered
|
||||
// log output emitted during driver shutdown is not lost. Particularly
|
||||
// important here: subscribe runs until Ctrl+C; without this, lines logged
|
||||
// during shutdown can be silently dropped.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,6 @@ public sealed class WriteCommand : AbLegacyCommandBase
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered
|
||||
// log output emitted during driver shutdown is not lost.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public static class SnapshotFormatter
|
||||
/// </summary>
|
||||
/// <param name="tagName">The tag name to include in the output.</param>
|
||||
/// <param name="snapshot">The data value snapshot to format.</param>
|
||||
/// <returns>The multi-line rendered snapshot.</returns>
|
||||
public static string Format(string tagName, DataValueSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
@@ -42,6 +43,7 @@ public static class SnapshotFormatter
|
||||
/// </summary>
|
||||
/// <param name="tagName">The tag name to include in the output.</param>
|
||||
/// <param name="result">The write result to format.</param>
|
||||
/// <returns>The rendered write-result line.</returns>
|
||||
public static string FormatWrite(string tagName, WriteResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
@@ -54,6 +56,7 @@ public static class SnapshotFormatter
|
||||
/// </summary>
|
||||
/// <param name="tagNames">The list of tag names to include as rows.</param>
|
||||
/// <param name="snapshots">The list of data value snapshots to format.</param>
|
||||
/// <returns>The rendered table as a multi-line string.</returns>
|
||||
public static string FormatTable(
|
||||
IReadOnlyList<string> tagNames, IReadOnlyList<DataValueSnapshot> snapshots)
|
||||
{
|
||||
|
||||
@@ -27,7 +27,6 @@ public sealed class ProbeCommand : FocasCommandBase
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
// Driver.FOCAS.Cli-003: validate numeric option ranges before any driver work.
|
||||
ValidateOptions();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
@@ -39,9 +38,6 @@ public sealed class ProbeCommand : FocasCommandBase
|
||||
Writable: false);
|
||||
var options = BuildOptions([probeTag]);
|
||||
|
||||
// Driver.FOCAS.Cli-004: `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
await using var driver = new FocasDriver(options, DriverInstanceId);
|
||||
try
|
||||
{
|
||||
@@ -59,9 +55,6 @@ public sealed class ProbeCommand : FocasCommandBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output
|
||||
// emitted during driver shutdown is not silently dropped (matching DriverCommandBase
|
||||
// docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT).
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,11 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
|
||||
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
|
||||
|
||||
/// <summary>Executes the read command against the FOCAS device.</summary>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
// Driver.FOCAS.Cli-003: validate numeric option ranges before any driver work.
|
||||
// Validate numeric option ranges before any driver work.
|
||||
ValidateOptions();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
@@ -40,7 +39,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
Writable: false);
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
// Driver.FOCAS.Cli-004: `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync
|
||||
// `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
await using var driver = new FocasDriver(options, DriverInstanceId);
|
||||
@@ -52,7 +51,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output
|
||||
// Flush Serilog before process exit so buffered log output
|
||||
// emitted during driver shutdown is not silently dropped (matching DriverCommandBase
|
||||
// docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT).
|
||||
FlushLogging();
|
||||
@@ -62,6 +61,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
/// <summary>Constructs a tag name from address and data type.</summary>
|
||||
/// <param name="address">The FOCAS address.</param>
|
||||
/// <param name="type">The data type.</param>
|
||||
/// <returns>The synthesised tag name in <c>address:type</c> form.</returns>
|
||||
internal static string SynthesiseTagName(string address, FocasDataType type)
|
||||
=> $"{address}:{type}";
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
// Driver.FOCAS.Cli-003: validate numeric option ranges (including the subscribe-only
|
||||
// Validate numeric option ranges (including the subscribe-only
|
||||
// --interval-ms) before any driver work so a zero/negative interval surfaces as a
|
||||
// clean CommandException rather than a tight-spinning poll loop.
|
||||
ValidateOptions(IntervalMs);
|
||||
@@ -44,12 +44,12 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
Writable: false);
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
// Driver.FOCAS.Cli-004: `await using` is the sole driver-disposal mechanism — FocasDriver.DisposeAsync
|
||||
// `await using` is the sole driver-disposal mechanism — FocasDriver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so a redundant ShutdownAsync(CancellationToken.None) in finally
|
||||
// ran shutdown twice. Only UnsubscribeAsync stays in the finally block — that's a subscription
|
||||
// lifecycle concern that is not part of driver disposal.
|
||||
await using var driver = new FocasDriver(options, DriverInstanceId);
|
||||
// Driver.FOCAS.Cli-002: serialize console writes from the PollGroupEngine background
|
||||
// Serialize console writes from the PollGroupEngine background
|
||||
// thread so overlapping poll ticks (and the "Subscribed to ..." banner from the CliFx
|
||||
// invocation thread) can't interleave partial lines.
|
||||
var writeLock = new object();
|
||||
@@ -58,7 +58,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
|
||||
// Driver.FOCAS.Cli-002: route every data-change event to the CliFx console (not
|
||||
// Route every data-change event to the CliFx console (not
|
||||
// System.Console — the analyzer flags it + IConsole is the testable abstraction).
|
||||
// The handler is synchronous because OnDataChange is raised from a driver
|
||||
// background thread; the IConsole.Output writer is not documented as thread-safe
|
||||
@@ -89,7 +89,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
|
||||
handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct);
|
||||
|
||||
// Driver.FOCAS.Cli-002: hold the lock around the banner write so the first
|
||||
// Hold the lock around the banner write so the first
|
||||
// poll-driven change line from the driver tick thread can't interleave with
|
||||
// this banner.
|
||||
lock (writeLock)
|
||||
@@ -110,7 +110,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
{
|
||||
if (handle is not null)
|
||||
{
|
||||
// Driver.FOCAS.Cli-002: stop the subscription before disposal — UnsubscribeAsync
|
||||
// Stop the subscription before disposal — UnsubscribeAsync
|
||||
// halts the poll-group ticker so no further OnDataChange events fire. The
|
||||
// anonymous handler is never explicitly removed via -=; instead, driver disposal
|
||||
// (via `await using` immediately after this finally) tears down the PollGroupEngine,
|
||||
@@ -119,9 +119,9 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
try { await driver.UnsubscribeAsync(handle, CancellationToken.None); }
|
||||
catch { /* teardown best-effort */ }
|
||||
}
|
||||
// Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output
|
||||
// emitted during driver shutdown is not silently dropped (matching DriverCommandBase
|
||||
// docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT).
|
||||
// Flush Serilog before process exit so buffered log output emitted during driver
|
||||
// shutdown is not silently dropped (matching DriverCommandBase docs and every
|
||||
// sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT).
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
// Driver.FOCAS.Cli-003: validate numeric option ranges before any driver work so
|
||||
// a zero/negative port/timeout surfaces as a clean CommandException rather than an
|
||||
// opaque downstream exception.
|
||||
// Validate numeric option ranges before any driver work so a zero/negative
|
||||
// port/timeout surfaces as a clean CommandException rather than an opaque
|
||||
// downstream exception.
|
||||
ValidateOptions();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
@@ -50,8 +50,8 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
|
||||
var parsed = ParseValue(Value, DataType);
|
||||
|
||||
// Driver.FOCAS.Cli-004: `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync already
|
||||
// invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
await using var driver = new FocasDriver(options, DriverInstanceId);
|
||||
try
|
||||
@@ -62,16 +62,16 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output
|
||||
// emitted during driver shutdown is not silently dropped (matching DriverCommandBase
|
||||
// docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT).
|
||||
// Flush Serilog before process exit so buffered log output emitted during driver
|
||||
// shutdown is not silently dropped (matching DriverCommandBase docs and every
|
||||
// sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT).
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parse <c>--value</c> per <see cref="FocasDataType"/>, invariant culture throughout.</summary>
|
||||
/// <remarks>
|
||||
/// Driver.FOCAS.Cli-001: numeric parses are wrapped so that malformed input
|
||||
/// Numeric parses are wrapped so that malformed input
|
||||
/// (<see cref="FormatException"/> / <see cref="OverflowException"/>) surfaces
|
||||
/// as a clean <see cref="CliFx.Exceptions.CommandException"/> rather than a raw
|
||||
/// .NET stack trace — matching the friendly message the Bit path already produces.
|
||||
|
||||
@@ -47,6 +47,7 @@ public abstract class FocasCommandBase : DriverCommandBase
|
||||
/// as <c>BadCommunicationError</c>.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tag definitions to include in the driver options.</param>
|
||||
/// <returns>A <see cref="FocasDriverOptions"/> configured with a single device and the supplied tags.</returns>
|
||||
protected FocasDriverOptions BuildOptions(IReadOnlyList<FocasTagDefinition> tags) => new()
|
||||
{
|
||||
Devices = [new FocasDeviceOptions(
|
||||
@@ -62,7 +63,7 @@ public abstract class FocasCommandBase : DriverCommandBase
|
||||
protected string DriverInstanceId => $"focas-cli-{CncHost}:{CncPort}";
|
||||
|
||||
/// <summary>
|
||||
/// Driver.FOCAS.Cli-003: validate numeric option ranges at the CLI boundary so a
|
||||
/// Validates numeric option ranges at the CLI boundary so a
|
||||
/// zero/negative <c>--cnc-port</c>, <c>--timeout-ms</c>, or <c>--interval-ms</c>
|
||||
/// surfaces as a clean <see cref="CliFx.Exceptions.CommandException"/> rather than
|
||||
/// either an opaque downstream exception (invalid <c>focas://host:<n></c> /
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class ProbeCommand : ModbusCommandBase
|
||||
var snapshot = await driver.ReadAsync(["__probe"], ct);
|
||||
var health = driver.GetHealth();
|
||||
|
||||
// Driver.Modbus.Cli-006: derive the headline verdict from BOTH the driver state
|
||||
// Derive the headline verdict from BOTH the driver state
|
||||
// AND the probe-read StatusCode so the operator never sees the previous
|
||||
// contradictory pair (`Health: Healthy` over a Bad snapshot line). The bare
|
||||
// driver state is still printed below for diagnostics, but the verdict is what
|
||||
@@ -61,21 +61,21 @@ public sealed class ProbeCommand : ModbusCommandBase
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Driver.Modbus.Cli-005: Ctrl+C during InitializeAsync — exit quietly so CliFx
|
||||
// Ctrl+C during InitializeAsync — exit quietly so CliFx
|
||||
// does not render a full stack trace for a user-initiated cancellation.
|
||||
await console.Output.WriteLineAsync("Cancelled.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines
|
||||
// Flush Serilog before process exit so buffered log lines
|
||||
// emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Modbus.Cli-006: combine the driver-side <see cref="DriverState"/> with the
|
||||
/// Combine the driver-side <see cref="DriverState"/> with the
|
||||
/// probe snapshot's OPC UA <c>StatusCode</c> into a single headline verdict. Order
|
||||
/// of precedence:
|
||||
/// <list type="number">
|
||||
|
||||
@@ -79,15 +79,15 @@ public sealed class ReadCommand : ModbusCommandBase
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Driver.Modbus.Cli-005: Ctrl+C during driver connect/read — exit quietly so
|
||||
// CliFx does not render a full stack trace for a user-initiated cancellation.
|
||||
// Ctrl+C during driver connect/read — exit quietly so CliFx does not render a
|
||||
// full stack trace for a user-initiated cancellation.
|
||||
await console.Output.WriteLineAsync("Cancelled.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines
|
||||
// emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern.
|
||||
// Flush Serilog before process exit so buffered log lines emitted during driver
|
||||
// shutdown are not lost. Matches Driver.AbCip.Cli pattern.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class SubscribeCommand : ModbusCommandBase
|
||||
"BigEndian (default) or WordSwap.")]
|
||||
public ModbusByteOrder ByteOrder { get; init; } = ModbusByteOrder.BigEndian;
|
||||
|
||||
// Driver.Modbus.Cli-001: subscribe previously lacked these three options that read and
|
||||
// subscribe previously lacked these three options that read and
|
||||
// write both expose. Without them, BitInRegister always watches bit 0 and String runs with
|
||||
// StringLength=0, silently producing wrong results for any subscriber using those types.
|
||||
/// <summary>Gets or sets the bit index for type=BitInRegister (0-15, LSB-first).</summary>
|
||||
@@ -63,7 +63,7 @@ public sealed class SubscribeCommand : ModbusCommandBase
|
||||
{
|
||||
ConfigureLogging();
|
||||
ValidateEndpoint();
|
||||
// Driver.Modbus.Cli-010: reject non-positive interval before opening the driver so
|
||||
// Reject non-positive interval before opening the driver so
|
||||
// the operator gets a clean error instead of a confusing PollGroupEngine failure.
|
||||
ValidateInterval(IntervalMs);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
@@ -82,7 +82,7 @@ public sealed class SubscribeCommand : ModbusCommandBase
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
await using var driver = new ModbusDriver(options, DriverInstanceId);
|
||||
// Driver.Modbus.Cli-004: serialize console writes from the PollGroupEngine background
|
||||
// Serialize console writes from the PollGroupEngine background
|
||||
// thread so overlapping poll ticks can't interleave partial lines.
|
||||
var writeLock = new object();
|
||||
ISubscriptionHandle? handle = null;
|
||||
@@ -90,7 +90,7 @@ public sealed class SubscribeCommand : ModbusCommandBase
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
|
||||
// Driver.Modbus.Cli-011: emit the banner BEFORE wiring OnDataChange so the
|
||||
// Emit the banner BEFORE wiring OnDataChange so the
|
||||
// main-thread WriteLineAsync cannot interleave with poll-thread change-event
|
||||
// writes. TextWriter.WriteLine is not guaranteed thread-safe; once the handler
|
||||
// is attached and SubscribeAsync starts, change events run on the poll thread.
|
||||
@@ -102,7 +102,7 @@ public sealed class SubscribeCommand : ModbusCommandBase
|
||||
// analyzer flags it + IConsole is the testable abstraction).
|
||||
driver.OnDataChange += (_, e) =>
|
||||
{
|
||||
// Driver.Modbus.Cli-004: swallow + log write failures so a transient stdout
|
||||
// Swallow + log write failures so a transient stdout
|
||||
// error (closed pipe, IO exception on a redirected stream) cannot tear down
|
||||
// the poll-engine background loop. Without this guard the unhandled
|
||||
// exception would fault the long-running subscribe.
|
||||
@@ -142,7 +142,7 @@ public sealed class SubscribeCommand : ModbusCommandBase
|
||||
catch { /* teardown best-effort */ }
|
||||
}
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines
|
||||
// Flush Serilog before process exit so buffered log lines
|
||||
// emitted just before Ctrl+C (e.g. during UnsubscribeAsync / ShutdownAsync) are
|
||||
// not lost. Matches the pattern in Driver.AbCip.Cli commands.
|
||||
FlushLogging();
|
||||
@@ -150,7 +150,7 @@ public sealed class SubscribeCommand : ModbusCommandBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Modbus.Cli-010: guards <c>--interval-ms</c> against zero or negative values.
|
||||
/// Guards <c>--interval-ms</c> against zero or negative values.
|
||||
/// A non-positive interval would produce a non-positive <see cref="TimeSpan"/> into
|
||||
/// <c>SubscribeAsync</c>; the CLI should fail fast with an actionable error rather
|
||||
/// than relying on the downstream <c>PollGroupEngine</c> to clamp the value.
|
||||
|
||||
@@ -59,7 +59,6 @@ public sealed class WriteCommand : ModbusCommandBase
|
||||
"For type=String: HighByteFirst (standard) or LowByteFirst (DirectLOGIC).")]
|
||||
public ModbusStringByteOrder StringByteOrder { get; init; } = ModbusStringByteOrder.HighByteFirst;
|
||||
|
||||
/// <summary>Executes the write command.</summary>
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -71,11 +70,6 @@ public sealed class WriteCommand : ModbusCommandBase
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
$"Region '{Region}' is read-only in the Modbus spec; writes require Coils or HoldingRegisters.");
|
||||
|
||||
// Driver.Modbus.Cli-002: coils are single-bit outputs — only Bool makes sense. A
|
||||
// non-boolean type (e.g. --region Coils --type UInt16) would silently coerce the value
|
||||
// to a boolean via Convert.ToBoolean, landing as ON for any non-zero value, with no
|
||||
// diagnostic. Reject it early so the operator sees a clear error rather than a silent
|
||||
// type-mismatch coerce.
|
||||
if (Region == ModbusRegion.Coils && DataType != ModbusDataType.Bool)
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
$"Region 'Coils' only supports boolean values (--type Bool). " +
|
||||
@@ -105,15 +99,11 @@ public sealed class WriteCommand : ModbusCommandBase
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Driver.Modbus.Cli-005: Ctrl+C during driver connect/write — exit quietly so
|
||||
// CliFx does not render a full stack trace for a user-initiated cancellation.
|
||||
await console.Output.WriteLineAsync("Cancelled.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
// Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines
|
||||
// emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public abstract class ModbusCommandBase : DriverCommandBase
|
||||
/// command against its own keep-alive reads.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tag definitions to include in the options.</param>
|
||||
/// <returns>A <see cref="ModbusDriverOptions"/> ready to hand to the driver constructor.</returns>
|
||||
protected ModbusDriverOptions BuildOptions(IReadOnlyList<ModbusTagDefinition> tags) => new()
|
||||
{
|
||||
Host = Host,
|
||||
@@ -66,7 +67,7 @@ public abstract class ModbusCommandBase : DriverCommandBase
|
||||
protected string DriverInstanceId => $"modbus-cli-{Host}:{Port}";
|
||||
|
||||
/// <summary>
|
||||
/// Driver.Modbus.Cli-003: validate the endpoint flags at parse time so the operator
|
||||
/// Validate the endpoint flags at parse time so the operator
|
||||
/// gets a clear CliFx error instead of an opaque socket / argument exception thrown
|
||||
/// deep inside the driver. Ranges:
|
||||
/// <list type="bullet">
|
||||
|
||||
@@ -37,11 +37,11 @@ public sealed class ProbeCommand : S7CommandBase
|
||||
Writable: false);
|
||||
var options = BuildOptions([probeTag]);
|
||||
|
||||
// Driver.S7.Cli-004: `await using` is the sole disposal mechanism — S7Driver.DisposeAsync
|
||||
// `await using` is the sole disposal mechanism — S7Driver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so the previous explicit ShutdownAsync(CancellationToken.None)
|
||||
// call in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
await using var driver = new S7Driver(options, DriverInstanceId);
|
||||
// Driver.S7.Cli-003: wrap the entire probe sequence so that a refused/unreachable TCP
|
||||
// Wrap the entire probe sequence so that a refused/unreachable TCP
|
||||
// connect still prints the structured Host/CPU/Health lines instead of crashing with a
|
||||
// full .NET stack trace. InitializeAsync sets health to Faulted with the exception
|
||||
// message before re-throwing, so GetHealth() always has something to report.
|
||||
|
||||
@@ -20,7 +20,6 @@ public sealed class ReadCommand : S7CommandBase
|
||||
IsRequired = true)]
|
||||
public string Address { get; init; } = default!;
|
||||
|
||||
// Driver.S7.Cli-002: help text reflects the types the driver currently implements.
|
||||
/// <summary>Gets the data type to interpret the address as.</summary>
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " +
|
||||
@@ -50,9 +49,9 @@ public sealed class ReadCommand : S7CommandBase
|
||||
StringLength: StringLength);
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
// Driver.S7.Cli-004: `await using` is the sole disposal mechanism — S7Driver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
// `await using` is the sole disposal mechanism — S7Driver.DisposeAsync already invokes
|
||||
// ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) in a
|
||||
// finally block would run shutdown twice.
|
||||
await using var driver = new S7Driver(options, DriverInstanceId);
|
||||
try
|
||||
{
|
||||
@@ -62,8 +61,8 @@ public sealed class ReadCommand : S7CommandBase
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Driver.S7.Cli-009: Ctrl+C during driver connect/read — exit quietly so
|
||||
// CliFx does not render a full stack trace for a user-initiated cancellation.
|
||||
// Ctrl+C during driver connect/read — exit quietly so CliFx does not render a
|
||||
// full stack trace for a user-initiated cancellation.
|
||||
await console.Output.WriteLineAsync("Cancelled.");
|
||||
}
|
||||
}
|
||||
@@ -71,6 +70,7 @@ public sealed class ReadCommand : S7CommandBase
|
||||
/// <summary>Tag-name key used internally. Address + type is already unique.</summary>
|
||||
/// <param name="address">The S7 address to encode in the tag name.</param>
|
||||
/// <param name="type">The data type to encode in the tag name.</param>
|
||||
/// <returns>A synthetic tag name combining the address and data type.</returns>
|
||||
internal static string SynthesiseTagName(string address, S7DataType type)
|
||||
=> $"{address}:{type}";
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ public sealed class SubscribeCommand : S7CommandBase
|
||||
public string Address { get; init; } = default!;
|
||||
|
||||
/// <summary>Gets the data type of the address.</summary>
|
||||
// Driver.S7.Cli-002: help text reflects the types the driver currently implements.
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " +
|
||||
"Wide types (Int64, UInt64, Float64/LReal, String, DateTime) are byte-anchored: address must use the B suffix " +
|
||||
@@ -44,23 +43,13 @@ public sealed class SubscribeCommand : S7CommandBase
|
||||
Writable: false);
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
// Driver.S7.Cli-004: `await using` is the sole driver-disposal mechanism — S7Driver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so a redundant ShutdownAsync(CancellationToken.None) in finally
|
||||
// ran shutdown twice. Only UnsubscribeAsync stays in the finally block — that's a subscription
|
||||
// lifecycle concern that is not part of driver disposal.
|
||||
await using var driver = new S7Driver(options, DriverInstanceId);
|
||||
// Driver.S7.Cli-011: serialize console writes from the PollGroupEngine background
|
||||
// thread so overlapping poll ticks cannot interleave partial lines on the output.
|
||||
var writeLock = new object();
|
||||
ISubscriptionHandle? handle = null;
|
||||
try
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
|
||||
// Driver.S7.Cli-007: route every data-change event to the CliFx console (not
|
||||
// System.Console — the analyzer flags it + IConsole is the testable abstraction).
|
||||
// The handler is synchronous because OnDataChange is raised from a driver
|
||||
// background thread; the IConsole.Output writer is thread-safe for line writes.
|
||||
driver.OnDataChange += (_, e) =>
|
||||
{
|
||||
var line = $"[{DateTime.UtcNow:HH:mm:ss.fff}] " +
|
||||
@@ -92,10 +81,6 @@ public sealed class SubscribeCommand : S7CommandBase
|
||||
try { await driver.UnsubscribeAsync(handle, CancellationToken.None); }
|
||||
catch { /* teardown best-effort */ }
|
||||
}
|
||||
// Driver.S7.Cli-010: flush Serilog before process exit so buffered log lines
|
||||
// emitted just before Ctrl+C (e.g. reconnect warnings from the PollGroupEngine)
|
||||
// are not lost on abrupt termination. DriverCommandBase.ConfigureLogging() docs
|
||||
// require this call in a finally block.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class WriteCommand : S7CommandBase
|
||||
public string Address { get; init; } = default!;
|
||||
|
||||
/// <summary>Gets or sets the data type of the value to write.</summary>
|
||||
// Driver.S7.Cli-002: help text reflects the types the driver currently implements.
|
||||
// Help text reflects the types the driver currently implements.
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " +
|
||||
"Wide types (Int64, UInt64, Float64/LReal, String, DateTime) are byte-anchored: address must use the B suffix " +
|
||||
@@ -57,7 +57,7 @@ public sealed class WriteCommand : S7CommandBase
|
||||
|
||||
var parsed = ParseValue(Value, DataType);
|
||||
|
||||
// Driver.S7.Cli-004: `await using` is the sole disposal mechanism — S7Driver.DisposeAsync
|
||||
// `await using` is the sole disposal mechanism — S7Driver.DisposeAsync
|
||||
// already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
await using var driver = new S7Driver(options, DriverInstanceId);
|
||||
@@ -69,7 +69,7 @@ public sealed class WriteCommand : S7CommandBase
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Driver.S7.Cli-009: Ctrl+C during driver connect/write — exit quietly so
|
||||
// Ctrl+C during driver connect/write — exit quietly so
|
||||
// CliFx does not render a full stack trace for a user-initiated cancellation.
|
||||
await console.Output.WriteLineAsync("Cancelled.");
|
||||
}
|
||||
@@ -77,7 +77,7 @@ public sealed class WriteCommand : S7CommandBase
|
||||
|
||||
/// <summary>Parse <c>--value</c> per <see cref="S7DataType"/>, invariant culture throughout.</summary>
|
||||
/// <remarks>
|
||||
/// Driver.S7.Cli-001: numeric and <see cref="DateTime"/> parses are wrapped so that
|
||||
/// Numeric and <see cref="DateTime"/> parses are wrapped so that
|
||||
/// malformed input (<see cref="FormatException"/> / <see cref="OverflowException"/>)
|
||||
/// surfaces as a clean <see cref="CliFx.Exceptions.CommandException"/> rather than a
|
||||
/// raw .NET stack trace — matching the friendly message the Bool path already produces.
|
||||
|
||||
@@ -52,6 +52,7 @@ public abstract class S7CommandBase : DriverCommandBase
|
||||
/// disabled — CLI runs are one-shot.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tag definitions to include in the options.</param>
|
||||
/// <returns>The constructed <see cref="S7DriverOptions"/>.</returns>
|
||||
protected S7DriverOptions BuildOptions(IReadOnlyList<S7TagDefinition> tags) => new()
|
||||
{
|
||||
Host = Host,
|
||||
@@ -68,7 +69,7 @@ public abstract class S7CommandBase : DriverCommandBase
|
||||
protected string DriverInstanceId => $"s7-cli-{Host}:{Port}";
|
||||
|
||||
/// <summary>
|
||||
/// Driver.S7.Cli-008: validate the endpoint flags at parse time so the operator
|
||||
/// Validate the endpoint flags at parse time so the operator
|
||||
/// gets a clear <see cref="CommandException"/> instead of an opaque socket or
|
||||
/// argument exception thrown deep inside the S7.Net stack.
|
||||
/// <list type="bullet">
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;
|
||||
/// Inherits from <see cref="TwinCATCommandBase"/> rather than
|
||||
/// <see cref="TwinCATTagCommandBase"/> so the <c>--poll-only</c> flag does NOT surface in
|
||||
/// <c>browse --help</c>: browse never subscribes, the flag would be a no-op, and the help
|
||||
/// text would mislead users (Driver.TwinCAT.Cli-004).
|
||||
/// text would mislead users.
|
||||
/// </remarks>
|
||||
[Command("browse", Description = "Enumerate controller symbols via the driver's DiscoverAsync walk.")]
|
||||
public sealed class BrowseCommand : TwinCATCommandBase
|
||||
@@ -91,6 +91,7 @@ public sealed class BrowseCommand : TwinCATCommandBase
|
||||
/// </summary>
|
||||
/// <param name="source">The source collection to filter.</param>
|
||||
/// <param name="prefix">The prefix to filter on, or null to keep everything.</param>
|
||||
/// <returns>The subset of <paramref name="source"/> whose browse name starts with <paramref name="prefix"/>.</returns>
|
||||
internal static List<(string BrowseName, DriverAttributeInfo Info)> FilterByPrefix(
|
||||
IReadOnlyList<(string BrowseName, DriverAttributeInfo Info)> source, string? prefix)
|
||||
=> source
|
||||
@@ -103,6 +104,8 @@ public sealed class BrowseCommand : TwinCATCommandBase
|
||||
/// </summary>
|
||||
/// <param name="matchedCount">The number of matched items.</param>
|
||||
/// <param name="max">The maximum number to show, or 0 for unbounded.</param>
|
||||
/// <returns>The number of items to print — <paramref name="matchedCount"/> if <paramref name="max"/>
|
||||
/// is unbounded, otherwise the smaller of the two.</returns>
|
||||
internal static int PrintLimit(int matchedCount, int max)
|
||||
=> max <= 0 ? matchedCount : Math.Min(max, matchedCount);
|
||||
|
||||
@@ -113,6 +116,7 @@ public sealed class BrowseCommand : TwinCATCommandBase
|
||||
/// authorization is enforced server-side.
|
||||
/// </summary>
|
||||
/// <param name="info">The attribute info to label.</param>
|
||||
/// <returns><c>"RO"</c> when the attribute is view-only; otherwise <c>"RW"</c>.</returns>
|
||||
internal static string AccessTag(DriverAttributeInfo info)
|
||||
=> info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW";
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed class SubscribeCommand : TwinCATTagCommandBase
|
||||
// write below and with subsequent change events if the PLC pushes faster than a
|
||||
// single console write completes. A TextWriter is not guaranteed thread-safe, so
|
||||
// we serialise every write through a lock to keep output clean for
|
||||
// screen-recorded bug-report timelines (Driver.TwinCAT.Cli-002).
|
||||
// screen-recorded bug-report timelines.
|
||||
var writeLock = new object();
|
||||
|
||||
driver.OnDataChange += (_, e) =>
|
||||
@@ -80,7 +80,7 @@ public sealed class SubscribeCommand : TwinCATTagCommandBase
|
||||
|
||||
handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct);
|
||||
|
||||
// Driver.TwinCAT.Cli-003: derive the banner mechanism from the actual subscription
|
||||
// Derive the banner mechanism from the actual subscription
|
||||
// handle the driver returned, not from --poll-only. The native ADS path tags its
|
||||
// handle with a "twincat-native-sub-*" DiagnosticId; anything else means we landed
|
||||
// on the shared PollGroupEngine. That way the line cannot disagree with what the
|
||||
|
||||
@@ -71,10 +71,11 @@ public sealed class WriteCommand : TwinCATTagCommandBase
|
||||
/// Parse <c>--value</c> per <see cref="TwinCATDataType"/>, invariant culture. Wraps
|
||||
/// <see cref="FormatException"/> and <see cref="OverflowException"/> in a
|
||||
/// <see cref="CliFx.Exceptions.CommandException"/> so the operator sees a clean one-line
|
||||
/// error instead of a raw stack trace (Driver.TwinCAT.Cli-008).
|
||||
/// error instead of a raw stack trace.
|
||||
/// </summary>
|
||||
/// <param name="raw">The raw string value to parse.</param>
|
||||
/// <param name="type">The target TwinCAT data type.</param>
|
||||
/// <returns>The parsed value boxed as an <see cref="object"/> matching the given <paramref name="type"/>.</returns>
|
||||
internal static object ParseValue(string raw, TwinCATDataType type)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -28,20 +28,16 @@ public abstract class TwinCATCommandBase : DriverCommandBase
|
||||
[CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")]
|
||||
public int TimeoutMs { get; init; } = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-operation timeout, projected from <see cref="TimeoutMs"/>. The CliFx
|
||||
/// <c>init</c> accessor required by the abstract base property is intentionally a
|
||||
/// no-op: <see cref="TimeoutMs"/> is the only source of truth, so any value an
|
||||
/// `init` initialiser supplies to this property directly is silently
|
||||
/// dropped. Do NOT add a backing field "fixing" the empty body — it would diverge
|
||||
/// from <see cref="TimeoutMs"/> and the two would drift on every refactor
|
||||
/// (Driver.TwinCAT.Cli-007).
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// Projected from TimeoutMs. The CliFx init accessor required by the abstract base property
|
||||
// is intentionally a no-op: TimeoutMs is the only source of truth, so any value an init
|
||||
// initialiser supplies to this property directly is silently dropped. Do NOT add a backing
|
||||
// field "fixing" the empty body — it would diverge from TimeoutMs and the two would drift on
|
||||
// every refactor.
|
||||
public override TimeSpan Timeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(TimeoutMs);
|
||||
init { /* see XML summary — driven by TimeoutMs */ }
|
||||
init { /* see comment above — driven by TimeoutMs */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,7 +53,7 @@ public abstract class TwinCATCommandBase : DriverCommandBase
|
||||
/// Validates the numeric options every TwinCAT CLI command shares (timeout + AMS port).
|
||||
/// Subclasses override and call <c>base.Validate()</c> first to add their own range
|
||||
/// checks. Throwing here surfaces a clean CliFx one-line error before the driver gets
|
||||
/// a chance to fail with an opaque transport error (Driver.TwinCAT.Cli-001).
|
||||
/// a chance to fail with an opaque transport error.
|
||||
/// </summary>
|
||||
protected virtual void Validate()
|
||||
{
|
||||
@@ -72,7 +68,7 @@ public abstract class TwinCATCommandBase : DriverCommandBase
|
||||
// ---- Test hooks ----
|
||||
// Protected members are exposed to the test assembly through these internal accessors so the
|
||||
// test project can cover Gateway / DriverInstanceId composition + range validation without
|
||||
// needing reflection on every assertion (Driver.TwinCAT.Cli-006).
|
||||
// needing reflection on every assertion.
|
||||
/// <summary>Gets the gateway string for testing.</summary>
|
||||
internal string GatewayForTest => Gateway;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli;
|
||||
/// single-tag <see cref="TwinCATDriverOptions"/> — <c>probe</c>, <c>read</c>, <c>write</c>,
|
||||
/// <c>subscribe</c>. Adds the <c>--poll-only</c> flag (relevant only when the driver is
|
||||
/// about to register native ADS notifications, which is why it does NOT live on the
|
||||
/// <c>browse</c> command — Driver.TwinCAT.Cli-004) and the <c>BuildOptions</c> helper that
|
||||
/// <c>browse</c> command) and the <c>BuildOptions</c> helper that
|
||||
/// assembles the driver-side options record.
|
||||
/// </summary>
|
||||
public abstract class TwinCATTagCommandBase : TwinCATCommandBase
|
||||
@@ -24,6 +24,7 @@ public abstract class TwinCATTagCommandBase : TwinCATCommandBase
|
||||
/// native notifications toggled by <see cref="PollOnly"/>.
|
||||
/// </summary>
|
||||
/// <param name="tags">Tag definitions for the driver.</param>
|
||||
/// <returns>The built <see cref="TwinCATDriverOptions"/>.</returns>
|
||||
protected TwinCATDriverOptions BuildOptions(IReadOnlyList<TwinCATTagDefinition> tags) => new()
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(
|
||||
@@ -39,6 +40,7 @@ public abstract class TwinCATTagCommandBase : TwinCATCommandBase
|
||||
// ---- Test hook ----
|
||||
/// <summary>Test hook that exposes BuildOptions for unit testing.</summary>
|
||||
/// <param name="tags">Tag definitions for the driver.</param>
|
||||
/// <returns>The built <see cref="TwinCATDriverOptions"/>.</returns>
|
||||
internal TwinCATDriverOptions BuildOptionsForTest(IReadOnlyList<TwinCATTagDefinition> tags)
|
||||
=> BuildOptions(tags);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user