@@ -1,4 +1,6 @@
|
||||
using System.Buffers.Binary;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
@@ -291,6 +293,15 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var now = DateTime.UtcNow;
|
||||
var results = new DataValueSnapshot[fullReferences.Count];
|
||||
|
||||
// PR F2-d (issue #266): pre-pass coalesces same-letter / same-path PMC byte reads
|
||||
// in this batch into one wire call per group via FocasPmcCoalescer. Each entry in
|
||||
// prefetched maps the original index to the byte slice + status from the range
|
||||
// read; the per-tag loop below short-circuits to the cached slice when present
|
||||
// and falls back to per-tag ReadAsync otherwise. Bit-addressed PMC tags
|
||||
// (R100.3) coalesce on their parent byte (R100); the bit extract still happens
|
||||
// in the existing decode path.
|
||||
var prefetched = await PrefetchPmcRangesAsync(fullReferences, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
for (var i = 0; i < fullReferences.Count; i++)
|
||||
{
|
||||
var reference = fullReferences[i];
|
||||
@@ -391,6 +402,26 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
await client.SetPathAsync(parsed.PathId, cancellationToken).ConfigureAwait(false);
|
||||
device.LastSetPath = parsed.PathId;
|
||||
}
|
||||
|
||||
// Coalesced PMC range fast path (issue #266) — if the prefetch pass already
|
||||
// satisfied this byte range as part of a coalesced wire call, decode from
|
||||
// the cached slice instead of issuing another per-tag ReadAsync.
|
||||
if (prefetched is not null && prefetched.TryGetValue(i, out var slice))
|
||||
{
|
||||
if (slice.Status != FocasStatusMapper.Good)
|
||||
{
|
||||
results[i] = new DataValueSnapshot(null, slice.Status, null, now);
|
||||
if (slice.Status != FocasStatusMapper.Good)
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
|
||||
$"FOCAS status 0x{slice.Status:X8} reading {reference}");
|
||||
continue;
|
||||
}
|
||||
var decoded = DecodePmcSlice(slice.Bytes, def.DataType, parsed.BitIndex);
|
||||
results[i] = new DataValueSnapshot(decoded, FocasStatusMapper.Good, now, now);
|
||||
_health = new DriverHealth(DriverState.Healthy, now, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
var (value, status) = parsed.Kind == FocasAreaKind.Diagnostic
|
||||
? await client.ReadDiagnosticAsync(
|
||||
parsed.Number, parsed.BitIndex ?? 0, def.DataType, cancellationToken).ConfigureAwait(false)
|
||||
@@ -1054,6 +1085,149 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
return new DataValueSnapshot(value, FocasStatusMapper.Good, now, now);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-pass for <see cref="ReadAsync"/> that groups same-letter / same-path PMC byte
|
||||
/// reads from <paramref name="fullReferences"/> into coalesced range reads (issue
|
||||
/// #266). Returns a map from the original batch index to the byte slice + status the
|
||||
/// wire returned for that tag's bytes; entries are absent for non-PMC tags + for tags
|
||||
/// whose coalesced read failed at the connect / set-path / parse step (those fall
|
||||
/// back to the existing per-tag dispatch in the main loop). Bit-addressed PMC tags
|
||||
/// (e.g. <c>R100.3</c>) are coalesced on their parent byte; the bit extract is
|
||||
/// deferred to <see cref="DecodePmcSlice"/> so the slice still satisfies them.
|
||||
/// </summary>
|
||||
private async Task<Dictionary<int, PmcSlice>?> PrefetchPmcRangesAsync(
|
||||
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
||||
{
|
||||
// Build per-device PMC request lists. Skip non-PMC tags + tags that don't resolve.
|
||||
Dictionary<string, List<(int Index, FocasAddress Parsed, FocasTagDefinition Def)>>? perDevice = null;
|
||||
for (var i = 0; i < fullReferences.Count; i++)
|
||||
{
|
||||
var reference = fullReferences[i];
|
||||
if (_statusNodesByName.ContainsKey(reference) ||
|
||||
_productionNodesByName.ContainsKey(reference) ||
|
||||
_modalNodesByName.ContainsKey(reference) ||
|
||||
_overrideNodesByName.ContainsKey(reference) ||
|
||||
_toolingNodesByName.ContainsKey(reference) ||
|
||||
_offsetNodesByName.ContainsKey(reference) ||
|
||||
_messagesNodesByName.ContainsKey(reference) ||
|
||||
_currentBlockNodesByName.ContainsKey(reference) ||
|
||||
_diagnosticsNodesByName.ContainsKey(reference))
|
||||
continue;
|
||||
if (!_tagsByName.TryGetValue(reference, out var def)) continue;
|
||||
if (!_devices.TryGetValue(def.DeviceHostAddress, out _)) continue;
|
||||
var parsed = FocasAddress.TryParse(def.Address);
|
||||
if (parsed is null) continue;
|
||||
if (parsed.Kind != FocasAreaKind.Pmc) continue;
|
||||
if (string.IsNullOrEmpty(parsed.PmcLetter)) continue;
|
||||
perDevice ??= new(StringComparer.OrdinalIgnoreCase);
|
||||
if (!perDevice.TryGetValue(def.DeviceHostAddress, out var list))
|
||||
perDevice[def.DeviceHostAddress] = list = new();
|
||||
list.Add((i, parsed, def));
|
||||
}
|
||||
if (perDevice is null) return null;
|
||||
|
||||
var result = new Dictionary<int, PmcSlice>();
|
||||
foreach (var (host, list) in perDevice)
|
||||
{
|
||||
// Single-tag PMC reads aren't worth coalescing — fall through to the per-tag
|
||||
// path so we don't pay the connect/set-path overhead twice.
|
||||
if (list.Count < 2) continue;
|
||||
var device = _devices[host];
|
||||
|
||||
IFocasClient client;
|
||||
try
|
||||
{
|
||||
client = await EnsureConnectedAsync(device, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch
|
||||
{
|
||||
continue; // per-tag path will surface BadCommunicationError per tag
|
||||
}
|
||||
|
||||
// Build coalescer requests. Skip path-out-of-range tags here so the per-tag
|
||||
// path can map them to BadOutOfRange consistently.
|
||||
var pmcRequests = new List<PmcAddressRequest>(list.Count);
|
||||
foreach (var (idx, parsed, def) in list)
|
||||
{
|
||||
if (parsed.PathId > 1 && device.PathCount > 0 && parsed.PathId > device.PathCount) continue;
|
||||
pmcRequests.Add(new PmcAddressRequest(
|
||||
parsed.PmcLetter!, parsed.PathId,
|
||||
parsed.Number, FocasPmcCoalescer.ByteWidth(def.DataType), idx));
|
||||
}
|
||||
if (pmcRequests.Count < 2) continue;
|
||||
|
||||
var groups = FocasPmcCoalescer.Plan(pmcRequests);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Members.Count < 2) continue; // single-member group — no benefit
|
||||
if (group.PathId != 1 && device.LastSetPath != group.PathId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.SetPathAsync(group.PathId, cancellationToken).ConfigureAwait(false);
|
||||
device.LastSetPath = group.PathId;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { continue; }
|
||||
}
|
||||
|
||||
byte[]? buffer; uint status;
|
||||
try
|
||||
{
|
||||
(buffer, status) = await client.ReadPmcRangeAsync(
|
||||
group.Letter, group.PathId, group.StartByte, group.ByteCount, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch
|
||||
{
|
||||
continue; // per-tag path will surface BadCommunicationError
|
||||
}
|
||||
|
||||
foreach (var member in group.Members)
|
||||
{
|
||||
if (status != FocasStatusMapper.Good || buffer is null)
|
||||
{
|
||||
result[member.OriginalIndex] = new PmcSlice(Array.Empty<byte>(), status);
|
||||
continue;
|
||||
}
|
||||
var slice = new byte[member.ByteWidth];
|
||||
if (member.Offset + member.ByteWidth <= buffer.Length)
|
||||
Array.Copy(buffer, member.Offset, slice, 0, member.ByteWidth);
|
||||
result[member.OriginalIndex] = new PmcSlice(slice, FocasStatusMapper.Good);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.Count == 0 ? null : result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode the bytes of a coalesced PMC slice into the boxed managed value the
|
||||
/// driver returns from <see cref="ReadAsync"/>. Mirrors the per-byte decode in
|
||||
/// <see cref="FwlibFocasClient"/>'s <c>ReadPmc</c> + the bit-extract path so the
|
||||
/// coalesced read is observably equivalent to a per-tag read.
|
||||
/// </summary>
|
||||
private static object? DecodePmcSlice(byte[] slice, FocasDataType type, int? bitIndex) => type switch
|
||||
{
|
||||
FocasDataType.Bit when bitIndex is int bi =>
|
||||
slice.Length > 0 ? (object)((slice[0] & (1 << bi)) != 0) : null,
|
||||
FocasDataType.Byte =>
|
||||
slice.Length > 0 ? (object)(sbyte)slice[0] : null,
|
||||
FocasDataType.Int16 =>
|
||||
slice.Length >= 2 ? (object)BinaryPrimitives.ReadInt16LittleEndian(slice) : null,
|
||||
FocasDataType.Int32 =>
|
||||
slice.Length >= 4 ? (object)BinaryPrimitives.ReadInt32LittleEndian(slice) : null,
|
||||
FocasDataType.Float32 =>
|
||||
slice.Length >= 4 ? (object)BinaryPrimitives.ReadSingleLittleEndian(slice) : null,
|
||||
FocasDataType.Float64 =>
|
||||
slice.Length >= 8 ? (object)BinaryPrimitives.ReadDoubleLittleEndian(slice) : null,
|
||||
_ => slice.Length > 0 ? (object)slice[0] : null,
|
||||
};
|
||||
|
||||
/// <summary>One coalesced PMC slice — bytes the wire returned for one batch member + the status.</summary>
|
||||
private readonly record struct PmcSlice(byte[] Bytes, uint Status);
|
||||
|
||||
/// <summary>
|
||||
/// Apply <c>cnc_getfigure</c>-derived decimal scaling to a raw position value.
|
||||
/// Returns <paramref name="raw"/> divided by <c>10^decimalPlaces</c> when the
|
||||
|
||||
Reference in New Issue
Block a user