From 45b23476fc5de3954eb35fb45586c2c7d864660c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 17 Jun 2026 06:34:05 -0400 Subject: [PATCH] fix(dcl): pad List writes to the Galaxy array's full declared size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Adapters/RealMxGatewayClient.cs | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs index ac7c7086..22b9de16 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs @@ -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; } + /// + /// 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. + /// + private async Task PadArrayToDeclaredSizeAsync(string tag, object? value, CancellationToken ct) + { + switch (value) + { + case IReadOnlyList v: return await PadAsync(tag, v, string.Empty, ct).ConfigureAwait(false); + case IReadOnlyList v: return await PadAsync(tag, v, 0, ct).ConfigureAwait(false); + case IReadOnlyList v: return await PadAsync(tag, v, 0L, ct).ConfigureAwait(false); + case IReadOnlyList v: return await PadAsync(tag, v, 0f, ct).ConfigureAwait(false); + case IReadOnlyList v: return await PadAsync(tag, v, 0d, ct).ConfigureAwait(false); + case IReadOnlyList v: return await PadAsync(tag, v, false, ct).ConfigureAwait(false); + case IReadOnlyList v: return await PadAsync(tag, v, default(DateTimeOffset), ct).ConfigureAwait(false); + case IReadOnlyList v: return await PadAsync(tag, v, default(DateTime), ct).ConfigureAwait(false); + default: return value; // scalar or unsupported — leave as-is + } + } + + private async Task> PadAsync(string tag, IReadOnlyList 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(size); + for (var i = 0; i < size; i++) + result.Add(i < values.Count ? values[i] : pad); + return result; + } + + /// Reads the tag's current array element count (its declared SAFEARRAY length), or 0 if not an array / unreadable. + private async Task 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, + }; + } + /// /// 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