Merge local master 0d49eaf7 into remed/lane-AbLegacy
Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -28,6 +28,7 @@ public sealed class ProbeCommand : FocasCommandBase
|
||||
{
|
||||
ConfigureLogging();
|
||||
ValidateOptions();
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var probeTag = new FocasTagDefinition(
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
|
||||
/// <summary>Gets the data type to interpret the address as.</summary>
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
|
||||
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -28,6 +28,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
ConfigureLogging();
|
||||
// Validate numeric option ranges before any driver work.
|
||||
ValidateOptions();
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = SynthesiseTagName(Address, DataType);
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
|
||||
/// <summary>Gets the data type of the address.</summary>
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
|
||||
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
|
||||
|
||||
/// <summary>Gets the polling interval in milliseconds.</summary>
|
||||
@@ -33,6 +33,7 @@ public sealed class SubscribeCommand : FocasCommandBase
|
||||
// --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);
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
|
||||
/// <summary>Gets the data type of the value to write.</summary>
|
||||
[CommandOption("type", 't', Description =
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
|
||||
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
|
||||
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
|
||||
|
||||
/// <summary>Gets the value to write.</summary>
|
||||
@@ -37,6 +37,7 @@ public sealed class WriteCommand : FocasCommandBase
|
||||
// port/timeout surfaces as a clean CommandException rather than an opaque
|
||||
// downstream exception.
|
||||
ValidateOptions();
|
||||
ValidateDataType(Address, DataType);
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
|
||||
|
||||
@@ -89,4 +89,17 @@ public abstract class FocasCommandBase : DriverCommandBase
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
$"--interval-ms must be positive (got {iv}).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refuses an <c>--address</c> / <c>--type</c> pair the driver cannot serve (String on a PMC / PARAM / MACRO
|
||||
/// address — the driver skips such a tag, so the command would only ever print BadNodeIdUnknown). Same
|
||||
/// rule as the driver and the AdminUI editor: <see cref="FocasTagDefinitionFactory.DescribeDataTypeMismatch"/>.
|
||||
/// </summary>
|
||||
/// <param name="address">The FOCAS address.</param>
|
||||
/// <param name="type">The data type.</param>
|
||||
protected static void ValidateDataType(string address, FocasDataType type)
|
||||
{
|
||||
if (FocasTagDefinitionFactory.DescribeDataTypeMismatch(type, address) is { } error)
|
||||
throw new CliFx.Exceptions.CommandException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ public sealed class ReadCommand : TwinCATTagCommandBase
|
||||
"String / WString / Time / Date / DateTime / TimeOfDay (default DInt).")]
|
||||
public TwinCATDataType DataType { get; init; } = TwinCATDataType.DInt;
|
||||
|
||||
/// <summary>Gets the element count for an array read; null reads a scalar.</summary>
|
||||
[CommandOption("array-length", Description =
|
||||
"Read the symbol as a 1-D array of this many --type elements (the leading sub-range when shorter).")]
|
||||
public int? ArrayLength { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -38,7 +43,8 @@ public sealed class ReadCommand : TwinCATTagCommandBase
|
||||
DeviceHostAddress: Gateway,
|
||||
SymbolPath: SymbolPath,
|
||||
DataType: DataType,
|
||||
Writable: false);
|
||||
Writable: false,
|
||||
ArrayLength: ArrayLength);
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
await using var driver = new TwinCATDriver(options, DriverInstanceId);
|
||||
@@ -47,6 +53,9 @@ public sealed class ReadCommand : TwinCATTagCommandBase
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
var snapshot = await driver.ReadAsync([tagName], ct);
|
||||
await console.Output.WriteLineAsync(SnapshotFormatter.Format(SymbolPath, snapshot[0]));
|
||||
if (snapshot[0].Value is Array elements)
|
||||
await console.Output.WriteLineAsync("Elements: [" + string.Join(", ",
|
||||
elements.Cast<object?>().Select(e => Convert.ToString(e, System.Globalization.CultureInfo.InvariantCulture))) + "]");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -30,6 +30,16 @@ public sealed class WriteCommand : TwinCATTagCommandBase
|
||||
IsRequired = true)]
|
||||
public string Value { get; init; } = default!;
|
||||
|
||||
/// <summary>Gets the element count for an array write; null writes a scalar.</summary>
|
||||
[CommandOption("array-length", Description =
|
||||
"Write the symbol as a 1-D array of this many --type elements. --value then carries the elements " +
|
||||
"separated by --separator (e.g. -v 1,2,3 --array-length 3); the count must match.")]
|
||||
public int? ArrayLength { get; init; }
|
||||
|
||||
/// <summary>Gets the element separator for an array write.</summary>
|
||||
[CommandOption("separator", Description = "Element separator for --array-length writes (default ',').")]
|
||||
public string Separator { get; init; } = ",";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -48,10 +58,13 @@ public sealed class WriteCommand : TwinCATTagCommandBase
|
||||
DeviceHostAddress: Gateway,
|
||||
SymbolPath: SymbolPath,
|
||||
DataType: DataType,
|
||||
Writable: true);
|
||||
Writable: true,
|
||||
ArrayLength: ArrayLength);
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
var parsed = ParseValue(Value, DataType);
|
||||
var parsed = ArrayLength is int count
|
||||
? ParseArrayValue(Value, DataType, count, Separator)
|
||||
: ParseValue(Value, DataType);
|
||||
|
||||
await using var driver = new TwinCATDriver(options, DriverInstanceId);
|
||||
try
|
||||
@@ -113,6 +126,27 @@ public sealed class WriteCommand : TwinCATTagCommandBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an array <c>--value</c>: <paramref name="separator"/>-separated elements, each parsed by
|
||||
/// <see cref="ParseValue"/>. The element count must equal <paramref name="count"/> — the driver
|
||||
/// refuses a mismatch with BadOutOfRange, but the CLI says so before touching the PLC.
|
||||
/// </summary>
|
||||
/// <param name="raw">The raw separated element list.</param>
|
||||
/// <param name="type">The element type.</param>
|
||||
/// <param name="count">The expected element count (<c>--array-length</c>).</param>
|
||||
/// <param name="separator">The element separator.</param>
|
||||
/// <returns>The parsed elements.</returns>
|
||||
internal static object[] ParseArrayValue(string raw, TwinCATDataType type, int count, string separator)
|
||||
{
|
||||
if (count < 1)
|
||||
throw new CliFx.Exceptions.CommandException("--array-length must be >= 1.");
|
||||
var parts = raw.Split(string.IsNullOrEmpty(separator) ? "," : separator);
|
||||
if (parts.Length != count)
|
||||
throw new CliFx.Exceptions.CommandException(
|
||||
$"--value has {parts.Length} element(s) but --array-length is {count}.");
|
||||
return [.. parts.Select(p => ParseValue(type is TwinCATDataType.String or TwinCATDataType.WString ? p : p.Trim(), type))];
|
||||
}
|
||||
|
||||
private static bool ParseBool(string raw) => raw.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"1" or "true" or "on" or "yes" => true,
|
||||
|
||||
Reference in New Issue
Block a user