using System.Globalization; using CliFx.Attributes; using CliFx.Infrastructure; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands; /// /// Write one value to a TwinCAT symbol. Structure writes refused — drop to driver config /// JSON for those. /// [Command("write", Description = "Write a single TwinCAT symbol.")] public sealed class WriteCommand : TwinCATTagCommandBase { /// Gets the symbol path to write. [CommandOption("symbol", 's', Description = "Symbol path — same format as `read`.", IsRequired = true)] public string SymbolPath { get; init; } = default!; /// Gets the data type of the symbol value. [CommandOption("type", 't', Description = "Bool / SInt / USInt / Int / UInt / DInt / UDInt / LInt / ULInt / Real / LReal / " + "String / WString / Time / Date / DateTime / TimeOfDay (default DInt).")] public TwinCATDataType DataType { get; init; } = TwinCATDataType.DInt; /// Gets the value to write. [CommandOption("value", 'v', Description = "Value to write. Parsed per --type (booleans accept true/false/1/0).", IsRequired = true)] public string Value { get; init; } = default!; /// public override async ValueTask ExecuteAsync(IConsole console) { Validate(); ConfigureLogging(); var ct = console.RegisterCancellationHandler(); if (DataType == TwinCATDataType.Structure) throw new CliFx.Exceptions.CommandException( "Structure (UDT) writes need an explicit member layout — drop to the driver's " + "config JSON for those. The CLI covers atomic types only."); var tagName = ReadCommand.SynthesiseTagName(SymbolPath, DataType); var tag = new TwinCATTagDefinition( Name: tagName, DeviceHostAddress: Gateway, SymbolPath: SymbolPath, DataType: DataType, Writable: true); var options = BuildOptions([tag]); var parsed = ParseValue(Value, DataType); await using var driver = new TwinCATDriver(options, DriverInstanceId); try { await driver.InitializeAsync("{}", ct); var results = await driver.WriteAsync([new WriteRequest(tagName, parsed)], ct); await console.Output.WriteLineAsync(SnapshotFormatter.FormatWrite(SymbolPath, results[0])); } finally { await driver.ShutdownAsync(CancellationToken.None); FlushLogging(); } } /// /// Parse --value per , invariant culture. Wraps /// and in a /// so the operator sees a clean one-line /// error instead of a raw stack trace. /// /// The raw string value to parse. /// The target TwinCAT data type. /// The parsed value boxed as an matching the given . internal static object ParseValue(string raw, TwinCATDataType type) { try { return type switch { TwinCATDataType.Bool => ParseBool(raw), TwinCATDataType.SInt => sbyte.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.USInt => byte.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.Int => short.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.UInt => ushort.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.DInt => int.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.UDInt => uint.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.LInt => long.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.ULInt => ulong.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.Real => float.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.LReal => double.Parse(raw, CultureInfo.InvariantCulture), TwinCATDataType.String or TwinCATDataType.WString => raw, // IEC 61131-3 time/date types are stored as UDINT on the wire — accept a numeric raw // value + let the caller handle the encoding semantics. TwinCATDataType.Time or TwinCATDataType.Date or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay => uint.Parse(raw, CultureInfo.InvariantCulture), _ => throw new CliFx.Exceptions.CommandException($"Unsupported DataType '{type}' for write."), }; } catch (CliFx.Exceptions.CommandException) { throw; // already a clean user-facing error (Bool parse, Structure, unsupported type) } catch (Exception ex) when (ex is FormatException or OverflowException) { throw new CliFx.Exceptions.CommandException( $"Cannot parse '{raw}' as {type}: {ex.Message}"); } } private static bool ParseBool(string raw) => raw.Trim().ToLowerInvariant() switch { "1" or "true" or "on" or "yes" => true, "0" or "false" or "off" or "no" => false, _ => throw new CliFx.Exceptions.CommandException( $"Boolean value '{raw}' is not recognised. Use true/false, 1/0, on/off, or yes/no."), }; }