diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipArrayReadPlanner.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipArrayReadPlanner.cs
new file mode 100644
index 0000000..2a60b5d
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipArrayReadPlanner.cs
@@ -0,0 +1,94 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
+
+///
+/// PR abcip-1.3 — issues one libplctag tag-create with ElementCount=N per Rockwell
+/// array-slice tag (Tag[0..N] in ), then decodes the
+/// contiguous buffer at element stride into N typed values. Mirrors the whole-UDT
+/// planner pattern (): pure shape — the planner never
+/// touches the runtime + never reads the PLC, the driver wires the runtime in.
+///
+///
+/// Stride is the natural Logix size of the element type (DInt = 4, Real = 4, LInt = 8).
+/// Bool / String / Structure slices aren't supported here — Logix packs BOOLs into a host
+/// byte (no fixed stride), STRING members carry a Length+DATA pair that's not a flat array,
+/// and structure arrays need the CIP Template Object reader (PR-tracked separately).
+///
+/// Output is a single object[] snapshot value containing the N decoded
+/// elements at indices 0..Count-1. Pairing with one slice tag = one snapshot keeps the
+/// ReadAsync 1:1 contract (one fullReference -> one snapshot) intact.
+///
+public static class AbCipArrayReadPlanner
+{
+ ///
+ /// Build the libplctag create-params + decode descriptor for a slice tag. Returns
+ /// null when the slice element type isn't supported under this declaration-only
+ /// decoder (Bool / String / Structure / unrecognised) — the driver falls back to the
+ /// scalar read path so the operator gets a clean per-element result instead.
+ ///
+ public static AbCipArrayReadPlan? TryBuild(
+ AbCipTagDefinition definition,
+ AbCipTagPath parsedPath,
+ AbCipTagCreateParams baseParams)
+ {
+ ArgumentNullException.ThrowIfNull(definition);
+ ArgumentNullException.ThrowIfNull(parsedPath);
+ ArgumentNullException.ThrowIfNull(baseParams);
+ if (parsedPath.Slice is null) return null;
+
+ if (!TryGetStride(definition.DataType, out var stride)) return null;
+
+ var slice = parsedPath.Slice;
+ var createParams = baseParams with
+ {
+ TagName = parsedPath.ToLibplctagSliceArrayName(),
+ ElementCount = slice.Count,
+ };
+
+ return new AbCipArrayReadPlan(definition.DataType, slice, stride, createParams);
+ }
+
+ ///
+ /// Decode .Count elements from at
+ /// element stride. Caller has already invoked
+ /// and confirmed == 0.
+ ///
+ public static object?[] Decode(AbCipArrayReadPlan plan, IAbCipTagRuntime runtime)
+ {
+ ArgumentNullException.ThrowIfNull(plan);
+ ArgumentNullException.ThrowIfNull(runtime);
+
+ var values = new object?[plan.Slice.Count];
+ for (var i = 0; i < plan.Slice.Count; i++)
+ values[i] = runtime.DecodeValueAt(plan.ElementType, i * plan.Stride, bitIndex: null);
+ return values;
+ }
+
+ private static bool TryGetStride(AbCipDataType type, out int stride)
+ {
+ switch (type)
+ {
+ case AbCipDataType.SInt: case AbCipDataType.USInt:
+ stride = 1; return true;
+ case AbCipDataType.Int: case AbCipDataType.UInt:
+ stride = 2; return true;
+ case AbCipDataType.DInt: case AbCipDataType.UDInt:
+ case AbCipDataType.Real: case AbCipDataType.Dt:
+ stride = 4; return true;
+ case AbCipDataType.LInt: case AbCipDataType.ULInt:
+ case AbCipDataType.LReal:
+ stride = 8; return true;
+ default:
+ stride = 0; return false;
+ }
+ }
+}
+
+///
+/// Plan output: the libplctag create-params for the single array-read tag plus the
+/// element-type / stride / slice metadata the decoder needs.
+///
+public sealed record AbCipArrayReadPlan(
+ AbCipDataType ElementType,
+ AbCipTagPathSlice Slice,
+ int Stride,
+ AbCipTagCreateParams CreateParams);
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
index 3ac96b7..a63d32b 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
@@ -358,6 +358,17 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return;
}
+ // PR abcip-1.3 — array-slice path. A tag whose TagPath ends in [N..M] dispatches to
+ // AbCipArrayReadPlanner: one libplctag tag-create with ElementCount=N issues one
+ // Rockwell array read; the contiguous buffer is decoded at element stride into a
+ // single snapshot whose Value is an object[] of the N elements.
+ var parsedPath = AbCipTagPath.TryParse(def.TagPath);
+ if (parsedPath?.Slice is not null)
+ {
+ await ReadSliceAsync(fb, def, parsedPath, device, results, now, ct).ConfigureAwait(false);
+ return;
+ }
+
try
{
var runtime = await EnsureTagRuntimeAsync(device, def, ct).ConfigureAwait(false);
@@ -373,8 +384,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return;
}
- var tagPath = AbCipTagPath.TryParse(def.TagPath);
- var bitIndex = tagPath?.BitIndex;
+ var bitIndex = parsedPath?.BitIndex;
var value = runtime.DecodeValue(def.DataType, bitIndex);
results[fb.OriginalIndex] = new DataValueSnapshot(value, AbCipStatusMapper.Good, now, now);
_health = new DriverHealth(DriverState.Healthy, now, null);
@@ -391,6 +401,89 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
}
}
+ ///
+ /// PR abcip-1.3 — slice read path. Builds an from the
+ /// parsed slice path, materialises a per-tag runtime keyed by the tag's full name (so
+ /// repeat reads reuse the same libplctag handle), issues one PLC array read, and
+ /// decodes the contiguous buffer into object?[] at element stride. Unsupported
+ /// element types fall back to .
+ ///
+ private async Task ReadSliceAsync(
+ AbCipUdtReadFallback fb, AbCipTagDefinition def, AbCipTagPath parsedPath,
+ DeviceState device, DataValueSnapshot[] results, DateTime now, CancellationToken ct)
+ {
+ var baseParams = new AbCipTagCreateParams(
+ Gateway: device.ParsedAddress.Gateway,
+ Port: device.ParsedAddress.Port,
+ CipPath: device.ParsedAddress.CipPath,
+ LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
+ TagName: parsedPath.ToLibplctagName(),
+ Timeout: _options.Timeout);
+
+ var plan = AbCipArrayReadPlanner.TryBuild(def, parsedPath, baseParams);
+ if (plan is null)
+ {
+ results[fb.OriginalIndex] = new DataValueSnapshot(null,
+ AbCipStatusMapper.BadNotSupported, null, now);
+ return;
+ }
+
+ try
+ {
+ var runtime = await EnsureSliceRuntimeAsync(device, def.Name, plan.CreateParams, ct)
+ .ConfigureAwait(false);
+ await runtime.ReadAsync(ct).ConfigureAwait(false);
+
+ var status = runtime.GetStatus();
+ if (status != 0)
+ {
+ results[fb.OriginalIndex] = new DataValueSnapshot(null,
+ AbCipStatusMapper.MapLibplctagStatus(status), null, now);
+ _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
+ $"libplctag status {status} reading slice {def.Name}");
+ return;
+ }
+
+ var values = AbCipArrayReadPlanner.Decode(plan, runtime);
+ results[fb.OriginalIndex] = new DataValueSnapshot(values, AbCipStatusMapper.Good, now, now);
+ _health = new DriverHealth(DriverState.Healthy, now, null);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ results[fb.OriginalIndex] = new DataValueSnapshot(null,
+ AbCipStatusMapper.BadCommunicationError, null, now);
+ _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
+ }
+ }
+
+ ///
+ /// Idempotently materialise a slice-read runtime. Slice runtimes share the device's
+ /// dict keyed by the tag's full name so repeated
+ /// reads reuse the same libplctag handle without re-creating the native tag every poll.
+ ///
+ private async Task EnsureSliceRuntimeAsync(
+ DeviceState device, string tagName, AbCipTagCreateParams createParams, CancellationToken ct)
+ {
+ if (device.Runtimes.TryGetValue(tagName, out var existing)) return existing;
+
+ var runtime = _tagFactory.Create(createParams);
+ try
+ {
+ await runtime.InitializeAsync(ct).ConfigureAwait(false);
+ }
+ catch
+ {
+ runtime.Dispose();
+ throw;
+ }
+ device.Runtimes[tagName] = runtime;
+ return runtime;
+ }
+
///
/// Task #194 — perform one whole-UDT read on the parent tag, then decode each
/// grouped member from the runtime's buffer at its computed byte offset. A per-group
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs
index 0891664..04a20d9 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs
@@ -20,7 +20,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
public sealed record AbCipTagPath(
string? ProgramScope,
IReadOnlyList Segments,
- int? BitIndex)
+ int? BitIndex,
+ AbCipTagPathSlice? Slice = null)
{
/// Rebuild the canonical Logix tag string.
public string ToLibplctagName()
@@ -37,10 +38,39 @@ public sealed record AbCipTagPath(
if (seg.Subscripts.Count > 0)
buf.Append('[').Append(string.Join(",", seg.Subscripts)).Append(']');
}
+ if (Slice is not null) buf.Append('[').Append(Slice.Start).Append("..").Append(Slice.End).Append(']');
if (BitIndex is not null) buf.Append('.').Append(BitIndex.Value);
return buf.ToString();
}
+ ///
+ /// Logix-symbol form for issuing a single libplctag tag-create that reads the slice as a
+ /// contiguous buffer — i.e. the bare array name (with the start subscript) without the
+ /// ..End suffix. The driver pairs this with
+ /// = to issue a single Rockwell array read.
+ ///
+ public string ToLibplctagSliceArrayName()
+ {
+ if (Slice is null) return ToLibplctagName();
+ var buf = new System.Text.StringBuilder();
+ if (ProgramScope is not null)
+ buf.Append("Program:").Append(ProgramScope).Append('.');
+
+ for (var i = 0; i < Segments.Count; i++)
+ {
+ if (i > 0) buf.Append('.');
+ var seg = Segments[i];
+ buf.Append(seg.Name);
+ if (seg.Subscripts.Count > 0)
+ buf.Append('[').Append(string.Join(",", seg.Subscripts)).Append(']');
+ }
+ // Anchor the read at the slice start; libplctag treats Name=Tag[0] + ElementCount=N as
+ // "read N consecutive elements starting at index 0", which is the exact Rockwell
+ // array-read semantic this PR is wiring up.
+ buf.Append('[').Append(Slice.Start).Append(']');
+ return buf.ToString();
+ }
+
///
/// Parse a Logix-symbolic tag reference. Returns null on a shape the parser
/// doesn't support — the driver surfaces that as a config-validation error rather than
@@ -91,8 +121,10 @@ public sealed record AbCipTagPath(
}
var segments = new List(parts.Count);
- foreach (var part in parts)
+ AbCipTagPathSlice? slice = null;
+ for (var partIdx = 0; partIdx < parts.Count; partIdx++)
{
+ var part = parts[partIdx];
var bracketIdx = part.IndexOf('[');
if (bracketIdx < 0)
{
@@ -104,6 +136,25 @@ public sealed record AbCipTagPath(
var name = part[..bracketIdx];
if (!IsValidIdent(name)) return null;
var inner = part[(bracketIdx + 1)..^1];
+
+ // Slice syntax `[N..M]` — only allowed on the LAST segment, must not coexist with
+ // multi-dim subscripts, must not be combined with bit-index, and requires M >= N.
+ // Any other shape is rejected so callers see a config-validation error rather than
+ // the driver attempting a best-effort scalar read.
+ if (inner.Contains(".."))
+ {
+ if (partIdx != parts.Count - 1) return null; // slice + sub-element
+ if (bitIndex is not null) return null; // slice + bit index
+ if (inner.Contains(',')) return null; // slice cannot be multi-dim
+ var parts2 = inner.Split("..", 2, StringSplitOptions.None);
+ if (parts2.Length != 2) return null;
+ if (!int.TryParse(parts2[0], out var sliceStart) || sliceStart < 0) return null;
+ if (!int.TryParse(parts2[1], out var sliceEnd) || sliceEnd < sliceStart) return null;
+ slice = new AbCipTagPathSlice(sliceStart, sliceEnd);
+ segments.Add(new AbCipTagPathSegment(name, []));
+ continue;
+ }
+
var subs = new List();
foreach (var tok in inner.Split(','))
{
@@ -115,7 +166,7 @@ public sealed record AbCipTagPath(
}
if (segments.Count == 0) return null;
- return new AbCipTagPath(programScope, segments, bitIndex);
+ return new AbCipTagPath(programScope, segments, bitIndex, slice);
}
private static bool IsValidIdent(string s)
@@ -130,3 +181,15 @@ public sealed record AbCipTagPath(
/// One path segment: a member name plus any numeric subscripts.
public sealed record AbCipTagPathSegment(string Name, IReadOnlyList Subscripts);
+
+///
+/// Inclusive-on-both-ends array slice carried on the trailing segment of an
+/// . Tag[0..15] parses to Start=0, End=15; the
+/// planner pairs this with libplctag's ElementCount attribute to issue a single
+/// Rockwell array read covering End - Start + 1 elements.
+///
+public sealed record AbCipTagPathSlice(int Start, int End)
+{
+ /// Total element count covered by the slice (inclusive both ends).
+ public int Count => End - Start + 1;
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs
index d6b496e..c720b13 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs
@@ -69,6 +69,10 @@ public interface IAbCipTagFactory
/// for STRING_20 / STRING_40 / STRING_80 UDTs). Threads through libplctag's
/// str_max_capacity attribute. null keeps libplctag's default 82-byte STRING
/// behaviour for back-compat.
+/// Optional libplctag ElementCount override — set to N
+/// to issue a Rockwell array read covering N consecutive elements starting at the
+/// subscripted index in . Drives PR abcip-1.3 array-slice support;
+/// null leaves libplctag's default scalar-element behaviour for back-compat.
public sealed record AbCipTagCreateParams(
string Gateway,
int Port,
@@ -76,4 +80,5 @@ public sealed record AbCipTagCreateParams(
string LibplctagPlcAttribute,
string TagName,
TimeSpan Timeout,
- int? StringMaxCapacity = null);
+ int? StringMaxCapacity = null,
+ int? ElementCount = null);
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
index 5389e6b..7bf8015 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
@@ -30,6 +30,11 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
// boundary. Null leaves libplctag at its default 82-byte STRING for back-compat.
if (p.StringMaxCapacity is int cap && cap > 0)
_tag.StringMaxCapacity = (uint)cap;
+ // PR abcip-1.3 — slice reads. Setting ElementCount tells libplctag to allocate a buffer
+ // covering N consecutive elements; the array-read planner pairs this with TagName=Tag[N]
+ // to issue one Rockwell array read for a [N..M] slice.
+ if (p.ElementCount is int n && n > 0)
+ _tag.ElementCount = n;
}
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayReadPlannerTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayReadPlannerTests.cs
new file mode 100644
index 0000000..2e5cdf8
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayReadPlannerTests.cs
@@ -0,0 +1,110 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class AbCipArrayReadPlannerTests
+{
+ private const string Device = "ab://10.0.0.5/1,0";
+
+ private static AbCipTagCreateParams BaseParams(string tagName) => new(
+ Gateway: "10.0.0.5",
+ Port: 44818,
+ CipPath: "1,0",
+ LibplctagPlcAttribute: "controllogix",
+ TagName: tagName,
+ Timeout: TimeSpan.FromSeconds(5));
+
+ [Fact]
+ public void TryBuild_emits_single_tag_create_with_element_count()
+ {
+ var def = new AbCipTagDefinition("DataSlice", Device, "Data[0..15]", AbCipDataType.DInt);
+ var parsed = AbCipTagPath.TryParse(def.TagPath)!;
+
+ var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..15]"));
+
+ plan.ShouldNotBeNull();
+ plan.ElementType.ShouldBe(AbCipDataType.DInt);
+ plan.Stride.ShouldBe(4);
+ plan.Slice.Count.ShouldBe(16);
+ plan.CreateParams.ElementCount.ShouldBe(16);
+ // Anchored at the slice start; libplctag reads N consecutive elements from there.
+ plan.CreateParams.TagName.ShouldBe("Data[0]");
+ }
+
+ [Fact]
+ public void TryBuild_returns_null_when_path_has_no_slice()
+ {
+ var def = new AbCipTagDefinition("Plain", Device, "Data[3]", AbCipDataType.DInt);
+ var parsed = AbCipTagPath.TryParse(def.TagPath)!;
+
+ AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[3]")).ShouldBeNull();
+ }
+
+ [Theory]
+ [InlineData(AbCipDataType.Bool)]
+ [InlineData(AbCipDataType.String)]
+ [InlineData(AbCipDataType.Structure)]
+ public void TryBuild_returns_null_for_unsupported_element_types(AbCipDataType type)
+ {
+ var def = new AbCipTagDefinition("Slice", Device, "Data[0..3]", type);
+ var parsed = AbCipTagPath.TryParse(def.TagPath)!;
+
+ AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..3]")).ShouldBeNull();
+ }
+
+ [Theory]
+ [InlineData(AbCipDataType.SInt, 1)]
+ [InlineData(AbCipDataType.Int, 2)]
+ [InlineData(AbCipDataType.DInt, 4)]
+ [InlineData(AbCipDataType.Real, 4)]
+ [InlineData(AbCipDataType.LInt, 8)]
+ [InlineData(AbCipDataType.LReal, 8)]
+ public void TryBuild_uses_natural_stride_per_element_type(AbCipDataType type, int expectedStride)
+ {
+ var def = new AbCipTagDefinition("Slice", Device, "Data[0..3]", type);
+ var parsed = AbCipTagPath.TryParse(def.TagPath)!;
+
+ var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..3]"))!;
+ plan.Stride.ShouldBe(expectedStride);
+ }
+
+ [Fact]
+ public void Decode_walks_buffer_at_element_stride()
+ {
+ var def = new AbCipTagDefinition("DataSlice", Device, "Data[0..3]", AbCipDataType.DInt);
+ var parsed = AbCipTagPath.TryParse(def.TagPath)!;
+ var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Data[0..3]"))!;
+
+ var fake = new FakeAbCipTag(plan.CreateParams);
+ // Stride == 4 for DInt, so offsets 0/4/8/12 hold the four element values.
+ fake.ValuesByOffset[0] = 100;
+ fake.ValuesByOffset[4] = 200;
+ fake.ValuesByOffset[8] = 300;
+ fake.ValuesByOffset[12] = 400;
+
+ var decoded = AbCipArrayReadPlanner.Decode(plan, fake);
+
+ decoded.Length.ShouldBe(4);
+ decoded.ShouldBe(new object?[] { 100, 200, 300, 400 });
+ }
+
+ [Fact]
+ public void Decode_preserves_slice_count_for_real_arrays()
+ {
+ var def = new AbCipTagDefinition("FloatSlice", Device, "Floats[2..5]", AbCipDataType.Real);
+ var parsed = AbCipTagPath.TryParse(def.TagPath)!;
+ var plan = AbCipArrayReadPlanner.TryBuild(def, parsed, BaseParams("Floats[2]"))!;
+
+ var fake = new FakeAbCipTag(plan.CreateParams);
+ fake.ValuesByOffset[0] = 1.5f;
+ fake.ValuesByOffset[4] = 2.5f;
+ fake.ValuesByOffset[8] = 3.5f;
+ fake.ValuesByOffset[12] = 4.5f;
+
+ var decoded = AbCipArrayReadPlanner.Decode(plan, fake);
+
+ decoded.ShouldBe(new object?[] { 1.5f, 2.5f, 3.5f, 4.5f });
+ }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs
index 91369bc..f83c1b3 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs
@@ -165,6 +165,55 @@ public sealed class AbCipDriverReadTests
p.TagName.ShouldBe("Program:P.Counter");
}
+ [Fact]
+ public async Task Slice_tag_reads_one_array_and_decodes_n_elements()
+ {
+ // PR abcip-1.3 — `Data[0..3]` slice routes through AbCipArrayReadPlanner: one libplctag
+ // tag-create at TagName="Data[0]" with ElementCount=4, single PLC read, contiguous
+ // buffer decoded at element stride into one snapshot whose Value is an object?[].
+ var (drv, factory) = NewDriver(
+ new AbCipTagDefinition("DataSlice", "ab://10.0.0.5/1,0", "Data[0..3]", AbCipDataType.DInt));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ factory.Customise = p =>
+ {
+ var t = new FakeAbCipTag(p);
+ t.ValuesByOffset[0] = 10;
+ t.ValuesByOffset[4] = 20;
+ t.ValuesByOffset[8] = 30;
+ t.ValuesByOffset[12] = 40;
+ return t;
+ };
+
+ var snapshots = await drv.ReadAsync(["DataSlice"], CancellationToken.None);
+
+ snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
+ var values = snapshots.Single().Value.ShouldBeOfType