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).
|
// supervisory subscription may already cover this handle — advise-once).
|
||||||
if (UseSupervisoryAdvise)
|
if (UseSupervisoryAdvise)
|
||||||
await EnsureSupervisoryAdvisedAsync(handle, ct).ConfigureAwait(false);
|
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
|
entries.Add(new WriteBulkEntry
|
||||||
{
|
{
|
||||||
ItemHandle = handle,
|
ItemHandle = handle,
|
||||||
Value = ToMxValue(value),
|
Value = ToMxValue(toWrite),
|
||||||
UserId = _writeUserId,
|
UserId = _writeUserId,
|
||||||
});
|
});
|
||||||
orderedTags.Add(tag);
|
orderedTags.Add(tag);
|
||||||
@@ -356,6 +362,76 @@ public sealed class RealMxGatewayClient : IMxGatewayClient
|
|||||||
return handle;
|
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>
|
/// <summary>
|
||||||
/// Ensures the item is advised in MXAccess SUPERVISORY mode (advise-once). A
|
/// 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
|
/// supervisory advise is what lets the gateway accept a write and it still
|
||||||
|
|||||||
Reference in New Issue
Block a user