feat(twincat): array and STRING writes through the codecs (U9)
Array tags were advertised writable but every write threw InvalidCastException in ConvertForWrite (reported BadTypeMismatch); STRING writes went through the Beckhoff marshaller with no length check against STRING(n) and the host's ANSI code page, which the read side deliberately avoids. - Array write: TwinCATArrayCodec.ToWriteElements converts exactly ArrayLength elements to the declared type (CLR lists convert - ScadaBridge writes int[] to Int16 nodes; a length mismatch or out-of-range element is BadOutOfRange, a scalar value BadTypeMismatch) before any wire traffic; the ADS client plans the layout from the resolved PLC array (TwinCATTypeGuard.CheckArray, as the read does), encodes with TwinCATArrayCodec.Encode - the exact inverse of Decode - and writes the raw bytes (new ITwinCATClient.WriteArrayAsync). - STRING / WSTRING write: encoded at the PLC's declared width by TwinCATStringCodec.EncodeString/EncodeWString (Windows-1252 or UTF-8 per target, NUL-padded, strict: an over-long value or unrepresentable character is OverflowException -> BadOutOfRange) and written as raw bytes. - TwinCAT CLI read/write gain --array-length (+ --separator) for the hardware gate. Refs #739 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -469,6 +469,23 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
{
|
||||
try
|
||||
{
|
||||
if (type is TwinCATDataType.String or TwinCATDataType.WString)
|
||||
{
|
||||
// A STRING / WSTRING is written as raw bytes at the PLC's own declared width, encoded by the
|
||||
// same codec the read decodes with: Windows-1252 (or UTF-8 when the target says so) rather
|
||||
// than the host's ANSI code page, NUL-padded so no stale tail survives, and refused
|
||||
// (OverflowException → BadOutOfRange) when it does not fit STRING(n).
|
||||
var (shape, _, symbol, symbolStatus) = await DescribeSymbolAsync(symbolPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (symbolStatus != TwinCATStatusMapper.Good) return symbolStatus;
|
||||
if (TwinCATTypeGuard.CheckRead(type, shape) is not null) return TwinCATStatusMapper.BadTypeMismatch;
|
||||
var text = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
var encoded = type == TwinCATDataType.WString
|
||||
? TwinCATStringCodec.EncodeWString(text, shape.ByteSize)
|
||||
: TwinCATStringCodec.EncodeString(text, shape.ByteSize, StringsAreUtf8());
|
||||
return await WriteRawAsync(symbolPath, symbol!, encoded, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var converted = ConvertForWrite(type, value);
|
||||
var result = await _client.WriteValueAsync(symbolPath, converted, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
@@ -482,6 +499,28 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<uint> WriteArrayAsync(string symbolPath, TwinCATDataType type, IReadOnlyList<object> elements,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Plan the layout from the RESOLVED PLC array exactly as the array read does, so the element
|
||||
// stride (incl. a STRING(n) element's n+1) and the strict element-type rule are shared.
|
||||
var (shape, array, symbol, symbolStatus) = await DescribeSymbolAsync(symbolPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (symbolStatus != TwinCATStatusMapper.Good) return symbolStatus;
|
||||
if (TwinCATTypeGuard.CheckArray(type, elements.Count, shape, array, out var layout) is not null)
|
||||
return TwinCATStatusMapper.BadTypeMismatch;
|
||||
var bytes = TwinCATArrayCodec.Encode(type, elements, layout, StringsAreUtf8());
|
||||
return await WriteRawAsync(symbolPath, symbol!, bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (AdsErrorException ex)
|
||||
{
|
||||
return MapAndSignal((uint)ex.ErrorCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<uint> WriteBitAsync(string parentPath, int bit, bool value, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -119,6 +119,23 @@ public interface ITwinCATClient : IDisposable
|
||||
object? value,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes an array tag: <paramref name="elements"/> (already converted to the declared element type by
|
||||
/// <see cref="TwinCATArrayCodec.ToWriteElements"/>) as raw bytes at the layout planned from the RESOLVED
|
||||
/// PLC array (<see cref="TwinCATTypeGuard.CheckArray"/>) and encoded by
|
||||
/// <see cref="TwinCATArrayCodec.Encode"/> — the exact inverse of the array read, so an array tag writes
|
||||
/// what it reads. Fewer elements than the PLC array writes its leading sub-range.
|
||||
/// </summary>
|
||||
/// <param name="symbolPath">The ADS symbol path of the array.</param>
|
||||
/// <param name="type">The declared element type.</param>
|
||||
/// <param name="elements">The converted elements (the authored <c>ArrayLength</c> of them).</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the write.</param>
|
||||
/// <returns>The mapped OPC UA status: Good; <c>BadTypeMismatch</c> when the declared element type /
|
||||
/// length cannot address the PLC array; the ADS write's status otherwise.</returns>
|
||||
/// <exception cref="OverflowException">A string element does not fit the PLC element's width.</exception>
|
||||
Task<uint> WriteArrayAsync(string symbolPath, TwinCATDataType type, IReadOnlyList<object> elements,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sets or clears one bit of an integer parent word — the write half of a BOOL-within-word tag
|
||||
/// (<c>MAIN.Flags.3</c>; ADS has no symbol for <c>WORD.N</c>). A read-modify-write of the parent's raw
|
||||
|
||||
@@ -19,6 +19,84 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
/// </remarks>
|
||||
internal static class TwinCATArrayCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a client-supplied array value into exactly <paramref name="count"/> elements of the
|
||||
/// declared type, each converted the way a scalar write converts (<see cref="AdsTwinCATClient.ConvertForWrite"/>
|
||||
/// — so TIME/DATE elements become their encoded <c>uint</c>). Any CLR collection is accepted and the
|
||||
/// element type is CONVERTED, not exact-matched: ScadaBridge writes an <c>int[]</c> to an Int16 node.
|
||||
/// </summary>
|
||||
/// <param name="declared">The tag's declared element type.</param>
|
||||
/// <param name="value">The written value — an array or list.</param>
|
||||
/// <param name="count">The tag's authored <c>ArrayLength</c>.</param>
|
||||
/// <returns>The converted elements, in order.</returns>
|
||||
/// <exception cref="InvalidCastException">The value is not a collection (a scalar, a string, null), or
|
||||
/// an element cannot convert.</exception>
|
||||
/// <exception cref="OverflowException">The element count differs from <paramref name="count"/>, or an
|
||||
/// element is outside the declared type's range.</exception>
|
||||
public static object[] ToWriteElements(TwinCATDataType declared, object? value, int count)
|
||||
{
|
||||
if (value is null or string || value is not System.Collections.IEnumerable items)
|
||||
throw new InvalidCastException(
|
||||
$"an array tag of {count} {declared} takes an array value, not {value?.GetType().Name ?? "null"}");
|
||||
var elements = new List<object>(count);
|
||||
foreach (var item in items) elements.Add(AdsTwinCATClient.ConvertForWrite(declared, item));
|
||||
if (elements.Count != count)
|
||||
throw new OverflowException($"wrote {elements.Count} elements to an array tag of arrayLength {count}");
|
||||
return [.. elements];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes converted elements (<see cref="ToWriteElements"/>) into the PLC's raw bytes at the planned
|
||||
/// layout — the exact inverse of <see cref="Decode"/>: little-endian at the element stride, a
|
||||
/// STRING / WSTRING element encoded by <see cref="TwinCATStringCodec"/> within its fixed stride.
|
||||
/// </summary>
|
||||
/// <param name="declared">The tag's declared element type.</param>
|
||||
/// <param name="elements">The converted elements; exactly <see cref="TwinCATArrayLayout.ElementCount"/>.</param>
|
||||
/// <param name="layout">The layout planned from the PLC array (<see cref="TwinCATTypeGuard.CheckArray"/>).</param>
|
||||
/// <param name="utf8">True when the target reports UTF-8 STRING data (else Windows-1252).</param>
|
||||
/// <returns>Exactly <see cref="TwinCATArrayLayout.ByteLength"/> bytes.</returns>
|
||||
/// <exception cref="ArgumentException">The element count differs from the layout.</exception>
|
||||
/// <exception cref="OverflowException">A string element does not fit its stride.</exception>
|
||||
public static byte[] Encode(TwinCATDataType declared, IReadOnlyList<object> elements, TwinCATArrayLayout layout, bool utf8)
|
||||
{
|
||||
if (layout.Stride <= 0 || elements.Count != layout.ElementCount)
|
||||
throw new ArgumentException(
|
||||
$"{elements.Count} elements do not match a layout of {layout.ElementCount} × {layout.Stride} B", nameof(elements));
|
||||
|
||||
var bytes = new byte[layout.ByteLength];
|
||||
var stride = layout.Stride;
|
||||
for (var i = 0; i < elements.Count; i++)
|
||||
{
|
||||
var slot = bytes.AsSpan(i * stride, stride);
|
||||
var e = elements[i];
|
||||
switch (declared)
|
||||
{
|
||||
case TwinCATDataType.Bool: slot[0] = Convert.ToBoolean(e) ? (byte)1 : (byte)0; break;
|
||||
case TwinCATDataType.SInt: slot[0] = unchecked((byte)Convert.ToSByte(e)); break;
|
||||
case TwinCATDataType.USInt: slot[0] = Convert.ToByte(e); break;
|
||||
case TwinCATDataType.Int: BinaryPrimitives.WriteInt16LittleEndian(slot, Convert.ToInt16(e)); break;
|
||||
case TwinCATDataType.UInt: BinaryPrimitives.WriteUInt16LittleEndian(slot, Convert.ToUInt16(e)); break;
|
||||
case TwinCATDataType.DInt: BinaryPrimitives.WriteInt32LittleEndian(slot, Convert.ToInt32(e)); break;
|
||||
case TwinCATDataType.UDInt or TwinCATDataType.Time or TwinCATDataType.TimeOfDay
|
||||
or TwinCATDataType.Date or TwinCATDataType.DateTime:
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(slot, Convert.ToUInt32(e)); break;
|
||||
case TwinCATDataType.LInt: BinaryPrimitives.WriteInt64LittleEndian(slot, Convert.ToInt64(e)); break;
|
||||
case TwinCATDataType.ULInt: BinaryPrimitives.WriteUInt64LittleEndian(slot, Convert.ToUInt64(e)); break;
|
||||
case TwinCATDataType.Real: BinaryPrimitives.WriteSingleLittleEndian(slot, Convert.ToSingle(e)); break;
|
||||
case TwinCATDataType.LReal: BinaryPrimitives.WriteDoubleLittleEndian(slot, Convert.ToDouble(e)); break;
|
||||
case TwinCATDataType.String:
|
||||
TwinCATStringCodec.EncodeString(Convert.ToString(e) ?? string.Empty, stride, utf8).CopyTo(slot);
|
||||
break;
|
||||
case TwinCATDataType.WString:
|
||||
TwinCATStringCodec.EncodeWString(Convert.ToString(e) ?? string.Empty, stride).CopyTo(slot);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"TwinCATDataType {declared} has no array element encoding");
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>Decodes <paramref name="layout"/>.ElementCount elements of <paramref name="declared"/>.</summary>
|
||||
/// <param name="declared">The tag's declared element type.</param>
|
||||
/// <param name="bytes">The PLC bytes from the array's first element (at least <see cref="TwinCATArrayLayout.ByteLength"/>).</param>
|
||||
|
||||
@@ -508,17 +508,26 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
|
||||
try
|
||||
{
|
||||
var client = await EnsureConnectedAsync(device, cancellationToken).ConfigureAwait(false);
|
||||
var parsed = TwinCATSymbolPath.TryParse(def.SymbolPath);
|
||||
var bitIndex = def.DataType == TwinCATDataType.Bool ? parsed?.BitIndex : null;
|
||||
// Shape checks run BEFORE any wire traffic (connect included): an array write needs exactly
|
||||
// ArrayLength elements, each converted to the declared element type (a CLR int[] to an Int16
|
||||
// array converts; it is not exact-matched) — a length mismatch / out-of-range element is
|
||||
// OverflowException (BadOutOfRange), a scalar value InvalidCastException (BadTypeMismatch).
|
||||
var bitValue = bitIndex is not null ? Convert.ToBoolean(w.Value) : false;
|
||||
var elements = bitIndex is null && def.ArrayLength is int arrayLength
|
||||
? TwinCATArrayCodec.ToWriteElements(def.DataType, w.Value, arrayLength)
|
||||
: null;
|
||||
|
||||
var client = await EnsureConnectedAsync(device, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// BOOL-within-word write — a read-modify-write of the parent word at the parent's OWN
|
||||
// width and on raw bytes (the client resolves the parent and checks the bit against it, the
|
||||
// notification path's model), under a per-parent lock so concurrent bit writers to the same
|
||||
// word don't lose each other's updates.
|
||||
if (def.DataType == TwinCATDataType.Bool && parsed?.BitIndex is int bit)
|
||||
if (bitIndex is int bit)
|
||||
{
|
||||
var parentPath = (parsed with { BitIndex = null }).ToAdsSymbolName();
|
||||
var bitValue = Convert.ToBoolean(w.Value);
|
||||
var parentPath = (parsed! with { BitIndex = null }).ToAdsSymbolName();
|
||||
var gate = _bitRmwLocks.GetOrAdd(
|
||||
$"{def.DeviceHostAddress}|{parentPath}", static _ => new SemaphoreSlim(1, 1));
|
||||
await gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -535,6 +544,15 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
}
|
||||
|
||||
var symbolName = parsed?.ToAdsSymbolName() ?? def.SymbolPath;
|
||||
if (elements is not null)
|
||||
{
|
||||
// Array tag: raw bytes encoded by TwinCATArrayCodec at the layout planned from the PLC
|
||||
// array — the inverse of the array read.
|
||||
results[i] = new WriteResult(await client.WriteArrayAsync(
|
||||
symbolName, def.DataType, elements, cancellationToken).ConfigureAwait(false));
|
||||
continue;
|
||||
}
|
||||
|
||||
var status = await client.WriteValueAsync(
|
||||
symbolName, def.DataType, parsed?.BitIndex, w.Value, cancellationToken).ConfigureAwait(false);
|
||||
results[i] = new WriteResult(status);
|
||||
|
||||
@@ -50,6 +50,57 @@ internal static class TwinCATStringCodec
|
||||
return (utf8 ? Encoding.UTF8 : Windows1252).GetString(bytes);
|
||||
}
|
||||
|
||||
// Strict encoders for the WRITE side: a character the target encoding cannot hold is refused rather
|
||||
// than silently replaced with '?' (the replacement would be written to the PLC as if it were the value).
|
||||
private static readonly Encoding StrictWindows1252 = CreateStrict(Windows1252.CodePage);
|
||||
private static readonly Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||
private static readonly Encoding StrictUtf16 = new UnicodeEncoding(bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: true);
|
||||
|
||||
private static Encoding CreateStrict(int codePage) =>
|
||||
Encoding.GetEncoding(codePage, EncoderFallback.ExceptionFallback, DecoderFallback.ReplacementFallback);
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a value into an IEC <c>STRING(n)</c> buffer of the PLC's declared width (n+1 bytes): the
|
||||
/// inverse of <see cref="DecodeString"/> — Windows-1252 unless <paramref name="utf8"/>, NUL-terminated
|
||||
/// and NUL-padded so no stale tail of a longer earlier value survives.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="byteWidth">The PLC symbol's byte width (n+1 for a <c>STRING(n)</c>).</param>
|
||||
/// <param name="utf8">True when the target reports UTF-8 string data.</param>
|
||||
/// <returns>Exactly <paramref name="byteWidth"/> bytes.</returns>
|
||||
/// <exception cref="OverflowException">The value does not fit <c>STRING(n)</c>, or holds a character the
|
||||
/// target encoding cannot represent.</exception>
|
||||
public static byte[] EncodeString(string value, int byteWidth, bool utf8) =>
|
||||
Encode(value, byteWidth, utf8 ? StrictUtf8 : StrictWindows1252, terminatorBytes: 1, "STRING");
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a value into an IEC <c>WSTRING(n)</c> buffer (UTF-16LE, 2(n+1) bytes): the inverse of
|
||||
/// <see cref="DecodeWString"/>, terminated by an aligned <c>U+0000</c> and zero-padded.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="byteWidth">The PLC symbol's byte width.</param>
|
||||
/// <returns>Exactly <paramref name="byteWidth"/> bytes.</returns>
|
||||
/// <exception cref="OverflowException">The value does not fit <c>WSTRING(n)</c>.</exception>
|
||||
public static byte[] EncodeWString(string value, int byteWidth) =>
|
||||
Encode(value, byteWidth, StrictUtf16, terminatorBytes: 2, "WSTRING");
|
||||
|
||||
private static byte[] Encode(string value, int byteWidth, Encoding encoding, int terminatorBytes, string kind)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
byte[] encoded;
|
||||
try { encoded = encoding.GetBytes(value); }
|
||||
catch (ArgumentException ex) // EncoderFallbackException: a character the encoding cannot hold
|
||||
{
|
||||
throw new OverflowException($"'{value}' cannot be represented in the PLC's {kind} encoding: {ex.Message}", ex);
|
||||
}
|
||||
if (encoded.Length + terminatorBytes > byteWidth)
|
||||
throw new OverflowException(
|
||||
$"'{value}' is {encoded.Length} B but the PLC {kind} holds {Math.Max(0, byteWidth - terminatorBytes)} B");
|
||||
var buffer = new byte[byteWidth];
|
||||
encoded.CopyTo(buffer, 0);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <summary>Decodes an IEC <c>WSTRING(n)</c> buffer (UTF-16LE): ends at the first aligned <c>U+0000</c>.</summary>
|
||||
/// <param name="bytes">The raw symbol bytes (2(n+1) for a <c>WSTRING(n)</c>).</param>
|
||||
/// <returns>The decoded value.</returns>
|
||||
|
||||
Reference in New Issue
Block a user