fix(dcl): pad List writes to the Galaxy array's full declared size
Even with correct array encoding (30d07b9), Ipsen MoveIn array writes still
hung: the Galaxy MES-receiver arrays are fixed-size SAFEARRAYs (e.g.
MoveInWorkOrderNumbers = SAFEARRAY(VT_BSTR) dimensions:[50]) and MXAccess only
accepts a write that supplies ALL slots. ScadaBridge sent just the N elements
the MES provided (1-2), so the COM write blocked. Verified on the live gateway:
a full-size (50) constructed array writes via WriteBulk in ~34ms; a short one
does not.
RealMxGatewayClient.WriteAsync now, for a list value, reads the tag's current
array to learn its slot count and pads the value to that length with
element-type defaults (empty string / 0 / false / default) — the caller's
values fill slots 0..N-1, the rest are cleared. The PLC reads the valid count
from a separate scalar (MoveInNumberWorkOrders). If the size can't be
determined (read fails / not an array) the value is written unpadded and a
warning is logged. Scalars are unaffected.
This commit is contained in:
@@ -150,10 +150,16 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
|
||||
// supervisory subscription may already cover this handle — advise-once).
|
||||
if (UseSupervisoryAdvise)
|
||||
await EnsureSupervisoryAdvisedAsync(handle, ct).ConfigureAwait(false);
|
||||
// MXAccess SAFEARRAY writes must supply the attribute's FULL declared
|
||||
// length; a short array (only the N elements the caller provided) is
|
||||
// rejected and the COM write blocks. Pad list values out to the live
|
||||
// array's slot count before encoding (callers signal the valid count
|
||||
// via a separate scalar, e.g. MoveInNumberWorkOrders).
|
||||
var toWrite = await PadArrayToDeclaredSizeAsync(tag, value, ct).ConfigureAwait(false);
|
||||
entries.Add(new WriteBulkEntry
|
||||
{
|
||||
ItemHandle = handle,
|
||||
Value = ToMxValue(value),
|
||||
Value = ToMxValue(toWrite),
|
||||
UserId = _writeUserId,
|
||||
});
|
||||
orderedTags.Add(tag);
|
||||
@@ -356,6 +362,76 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MXAccess fixed-size SAFEARRAY attributes only accept a write that supplies
|
||||
/// the whole array; a short list (just the caller's N elements) is rejected and
|
||||
/// the COM write blocks. For a list value, read the tag's current array to learn
|
||||
/// its slot count and pad the value to that length with element-type defaults
|
||||
/// (empty string / 0 / false / default) — the caller's values fill slots 0..N-1,
|
||||
/// the remainder are cleared. Scalars (and a non-array / unreadable tag) pass
|
||||
/// through unchanged.
|
||||
/// </summary>
|
||||
private async Task<object?> PadArrayToDeclaredSizeAsync(string tag, object? value, CancellationToken ct)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case IReadOnlyList<string> v: return await PadAsync(tag, v, string.Empty, ct).ConfigureAwait(false);
|
||||
case IReadOnlyList<int> v: return await PadAsync(tag, v, 0, ct).ConfigureAwait(false);
|
||||
case IReadOnlyList<long> v: return await PadAsync(tag, v, 0L, ct).ConfigureAwait(false);
|
||||
case IReadOnlyList<float> v: return await PadAsync(tag, v, 0f, ct).ConfigureAwait(false);
|
||||
case IReadOnlyList<double> v: return await PadAsync(tag, v, 0d, ct).ConfigureAwait(false);
|
||||
case IReadOnlyList<bool> v: return await PadAsync(tag, v, false, ct).ConfigureAwait(false);
|
||||
case IReadOnlyList<DateTimeOffset> v: return await PadAsync(tag, v, default(DateTimeOffset), ct).ConfigureAwait(false);
|
||||
case IReadOnlyList<DateTime> v: return await PadAsync(tag, v, default(DateTime), ct).ConfigureAwait(false);
|
||||
default: return value; // scalar or unsupported — leave as-is
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<T>> PadAsync<T>(string tag, IReadOnlyList<T> values, T pad, CancellationToken ct)
|
||||
{
|
||||
var size = await GetArraySlotCountAsync(tag, ct).ConfigureAwait(false);
|
||||
if (size <= 0)
|
||||
{
|
||||
// Couldn't determine the declared size (read failed or tag is not an
|
||||
// array). Write what we have rather than blocking the caller; if the
|
||||
// node really is a short SAFEARRAY the gateway will surface the error.
|
||||
_logger.LogWarning("Could not determine array size for '{Tag}'; writing {Count} element(s) unpadded", tag, values.Count);
|
||||
return values;
|
||||
}
|
||||
|
||||
if (values.Count > size)
|
||||
_logger.LogWarning("Array write for '{Tag}' has {Count} elements but the node holds {Size}; truncating", tag, values.Count, size);
|
||||
|
||||
var result = new List<T>(size);
|
||||
for (var i = 0; i < size; i++)
|
||||
result.Add(i < values.Count ? values[i] : pad);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Reads the tag's current array element count (its declared SAFEARRAY length), or 0 if not an array / unreadable.</summary>
|
||||
private async Task<int> GetArraySlotCountAsync(string tag, CancellationToken ct)
|
||||
{
|
||||
var reads = await _session!
|
||||
.ReadBulkAsync(_serverHandle, new[] { tag }, TimeSpan.FromMilliseconds(_readTimeoutMs), ct)
|
||||
.ConfigureAwait(false);
|
||||
var v = reads.Count > 0 ? reads[0].Value : null;
|
||||
if (v is null || v.KindCase != MxValue.KindOneofCase.ArrayValue)
|
||||
return 0;
|
||||
|
||||
var a = v.ArrayValue;
|
||||
return a.ValuesCase switch
|
||||
{
|
||||
MxArray.ValuesOneofCase.StringValues => a.StringValues.Values.Count,
|
||||
MxArray.ValuesOneofCase.Int32Values => a.Int32Values.Values.Count,
|
||||
MxArray.ValuesOneofCase.Int64Values => a.Int64Values.Values.Count,
|
||||
MxArray.ValuesOneofCase.FloatValues => a.FloatValues.Values.Count,
|
||||
MxArray.ValuesOneofCase.DoubleValues => a.DoubleValues.Values.Count,
|
||||
MxArray.ValuesOneofCase.BoolValues => a.BoolValues.Values.Count,
|
||||
MxArray.ValuesOneofCase.TimestampValues => a.TimestampValues.Values.Count,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the item is advised in MXAccess SUPERVISORY mode (advise-once). A
|
||||
/// supervisory advise is what lets the gateway accept a write and it still
|
||||
|
||||
Reference in New Issue
Block a user