152
src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasPmcCoalescer.cs
Normal file
152
src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasPmcCoalescer.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
|
||||
/// <summary>
|
||||
/// One PMC byte-level read request a caller wants to satisfy. <see cref="ByteWidth"/> is
|
||||
/// how many consecutive PMC bytes the caller's tag occupies (e.g. Bit/Byte = 1, Int16 = 2,
|
||||
/// Int32/Float32 = 4, Float64 = 8). <see cref="OriginalIndex"/> is the caller's row index
|
||||
/// in the batch — the coalescer carries it through to the planned group so the driver can
|
||||
/// fan-out the slice back to the original snapshot slot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bit-addressed PMC tags (e.g. <c>R100.3</c>) supply their parent byte (<c>100</c>) +
|
||||
/// <c>ByteWidth = 1</c>; the slice-then-mask happens in the existing decode path, so
|
||||
/// the coalescer doesn't need to know about the bit index.
|
||||
/// </remarks>
|
||||
public sealed record PmcAddressRequest(
|
||||
string Letter,
|
||||
int PathId,
|
||||
int ByteNumber,
|
||||
int ByteWidth,
|
||||
int OriginalIndex);
|
||||
|
||||
/// <summary>
|
||||
/// One member of a coalesced PMC range — the original index + the byte offset within
|
||||
/// the planned range buffer where the member's bytes start. The caller slices
|
||||
/// <c>buffer[Offset .. Offset + ByteWidth]</c> to recover the per-tag wire-shape bytes.
|
||||
/// </summary>
|
||||
public sealed record PmcRangeMember(int OriginalIndex, int Offset, int ByteWidth);
|
||||
|
||||
/// <summary>
|
||||
/// One coalesced PMC range — a single FOCAS <c>pmc_rdpmcrng</c> wire call satisfies
|
||||
/// every member. <see cref="ByteCount"/> is bounded by <see cref="FocasPmcCoalescer.MaxRangeBytes"/>.
|
||||
/// </summary>
|
||||
public sealed record PmcRangeGroup(
|
||||
string Letter,
|
||||
int PathId,
|
||||
int StartByte,
|
||||
int ByteCount,
|
||||
IReadOnlyList<PmcRangeMember> Members);
|
||||
|
||||
/// <summary>
|
||||
/// Plans one or more coalesced PMC range reads from a flat batch of per-tag requests.
|
||||
/// Same-letter / same-path requests whose byte ranges overlap or whose gap is no larger
|
||||
/// than <see cref="MaxBridgeGap"/> are merged into a single wire call up to a
|
||||
/// <see cref="MaxRangeBytes"/> cap (issue #266).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The cap matches the conservative ceiling Fanuc spec lists for
|
||||
/// <c>pmc_rdpmcrng</c> — most controllers accept larger ranges but 256 is the lowest
|
||||
/// common denominator across 0i / 16i / 30i firmware. Splitting on the cap is fine —
|
||||
/// each partition still saves N-1 round trips relative to per-byte reads.</para>
|
||||
///
|
||||
/// <para>The <see cref="MaxBridgeGap"/> = 16 mirrors the Modbus coalescer's bridge
|
||||
/// policy: small gaps are cheaper to over-read (one extra wire call vs. several short
|
||||
/// ones) but unbounded bridging would pull large unused regions over the wire on sparse
|
||||
/// PMC layouts.</para>
|
||||
/// </remarks>
|
||||
public static class FocasPmcCoalescer
|
||||
{
|
||||
/// <summary>Maximum bytes per coalesced range — conservative ceiling for older Fanuc firmware.</summary>
|
||||
public const int MaxRangeBytes = 256;
|
||||
|
||||
/// <summary>Maximum gap (in bytes) bridged between consecutive sub-requests within a group.</summary>
|
||||
public const int MaxBridgeGap = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Plan range reads from <paramref name="addresses"/>. Group key is
|
||||
/// <c>(Letter, PathId)</c>. Within a group, requests are sorted by start byte then
|
||||
/// greedily packed into ranges that respect <see cref="MaxRangeBytes"/> +
|
||||
/// <see cref="MaxBridgeGap"/>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<PmcRangeGroup> Plan(IEnumerable<PmcAddressRequest> addresses)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(addresses);
|
||||
|
||||
var groups = new List<PmcRangeGroup>();
|
||||
var byKey = addresses
|
||||
.Where(a => !string.IsNullOrEmpty(a.Letter) && a.ByteWidth > 0 && a.ByteNumber >= 0)
|
||||
.GroupBy(a => (Letter: a.Letter.ToUpperInvariant(), a.PathId));
|
||||
|
||||
foreach (var key in byKey)
|
||||
{
|
||||
var sorted = key.OrderBy(a => a.ByteNumber).ThenBy(a => a.OriginalIndex).ToList();
|
||||
var pending = new List<PmcAddressRequest>();
|
||||
var rangeStart = -1;
|
||||
var rangeEnd = -1;
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (pending.Count == 0) return;
|
||||
var members = pending.Select(p =>
|
||||
new PmcRangeMember(p.OriginalIndex, p.ByteNumber - rangeStart, p.ByteWidth)).ToList();
|
||||
groups.Add(new PmcRangeGroup(key.Key.Letter, key.Key.PathId, rangeStart,
|
||||
rangeEnd - rangeStart + 1, members));
|
||||
pending.Clear();
|
||||
rangeStart = -1;
|
||||
rangeEnd = -1;
|
||||
}
|
||||
|
||||
foreach (var req in sorted)
|
||||
{
|
||||
var reqStart = req.ByteNumber;
|
||||
var reqEnd = req.ByteNumber + req.ByteWidth - 1;
|
||||
if (pending.Count == 0)
|
||||
{
|
||||
rangeStart = reqStart;
|
||||
rangeEnd = reqEnd;
|
||||
pending.Add(req);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bridge if the gap between the existing range end + this request's start is
|
||||
// within the bridge cap. Overlapping or contiguous ranges always bridge
|
||||
// (gap <= 0). The cap is enforced on the projected union: extending the range
|
||||
// must not exceed MaxRangeBytes from rangeStart.
|
||||
var gap = reqStart - rangeEnd - 1;
|
||||
var projectedEnd = Math.Max(rangeEnd, reqEnd);
|
||||
var projectedSize = projectedEnd - rangeStart + 1;
|
||||
if (gap <= MaxBridgeGap && projectedSize <= MaxRangeBytes)
|
||||
{
|
||||
rangeEnd = projectedEnd;
|
||||
pending.Add(req);
|
||||
}
|
||||
else
|
||||
{
|
||||
Flush();
|
||||
rangeStart = reqStart;
|
||||
rangeEnd = reqEnd;
|
||||
pending.Add(req);
|
||||
}
|
||||
}
|
||||
|
||||
Flush();
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of consecutive PMC bytes a tag of <paramref name="type"/> occupies on
|
||||
/// the wire. Used by the driver to populate <see cref="PmcAddressRequest.ByteWidth"/>
|
||||
/// before planning. Bit-addressed tags supply <c>1</c> here — the bit-extract happens
|
||||
/// in the decode path after the slice.
|
||||
/// </summary>
|
||||
public static int ByteWidth(FocasDataType type) => type switch
|
||||
{
|
||||
FocasDataType.Bit or FocasDataType.Byte => 1,
|
||||
FocasDataType.Int16 => 2,
|
||||
FocasDataType.Int32 or FocasDataType.Float32 => 4,
|
||||
FocasDataType.Float64 => 8,
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user