namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; /// /// One PMC byte-level read request a caller wants to satisfy. is /// how many consecutive PMC bytes the caller's tag occupies (e.g. Bit/Byte = 1, Int16 = 2, /// Int32/Float32 = 4, Float64 = 8). 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. /// /// /// Bit-addressed PMC tags (e.g. R100.3) supply their parent byte (100) + /// ByteWidth = 1; the slice-then-mask happens in the existing decode path, so /// the coalescer doesn't need to know about the bit index. /// public sealed record PmcAddressRequest( string Letter, int PathId, int ByteNumber, int ByteWidth, int OriginalIndex); /// /// 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 /// buffer[Offset .. Offset + ByteWidth] to recover the per-tag wire-shape bytes. /// public sealed record PmcRangeMember(int OriginalIndex, int Offset, int ByteWidth); /// /// One coalesced PMC range — a single FOCAS pmc_rdpmcrng wire call satisfies /// every member. is bounded by . /// public sealed record PmcRangeGroup( string Letter, int PathId, int StartByte, int ByteCount, IReadOnlyList Members); /// /// 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 are merged into a single wire call up to a /// cap (issue #266). /// /// /// The cap matches the conservative ceiling Fanuc spec lists for /// pmc_rdpmcrng — 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. /// /// The = 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. /// public static class FocasPmcCoalescer { /// Maximum bytes per coalesced range — conservative ceiling for older Fanuc firmware. public const int MaxRangeBytes = 256; /// Maximum gap (in bytes) bridged between consecutive sub-requests within a group. public const int MaxBridgeGap = 16; /// /// Plan range reads from . Group key is /// (Letter, PathId). Within a group, requests are sorted by start byte then /// greedily packed into ranges that respect + /// . /// public static IReadOnlyList Plan(IEnumerable addresses) { ArgumentNullException.ThrowIfNull(addresses); var groups = new List(); 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(); 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; } /// /// The number of consecutive PMC bytes a tag of occupies on /// the wire. Used by the driver to populate /// before planning. Bit-addressed tags supply 1 here — the bit-extract happens /// in the decode path after the slice. /// 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, }; }