Merge pull request '[abcip] AbCip — STRINGnn variant decoding' (#318) from auto/abcip/1.2 into auto/driver-gaps
This commit was merged in pull request #318.
This commit is contained in:
@@ -134,7 +134,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
TagPath: $"{tag.TagPath}.{member.Name}",
|
TagPath: $"{tag.TagPath}.{member.Name}",
|
||||||
DataType: member.DataType,
|
DataType: member.DataType,
|
||||||
Writable: member.Writable,
|
Writable: member.Writable,
|
||||||
WriteIdempotent: member.WriteIdempotent);
|
WriteIdempotent: member.WriteIdempotent,
|
||||||
|
StringLength: member.StringLength);
|
||||||
_tagsByName[memberTag.Name] = memberTag;
|
_tagsByName[memberTag.Name] = memberTag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -633,7 +634,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
|||||||
CipPath: device.ParsedAddress.CipPath,
|
CipPath: device.ParsedAddress.CipPath,
|
||||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||||
TagName: parsed.ToLibplctagName(),
|
TagName: parsed.ToLibplctagName(),
|
||||||
Timeout: _options.Timeout));
|
Timeout: _options.Timeout,
|
||||||
|
StringMaxCapacity: def.DataType == AbCipDataType.String ? def.StringLength : null));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ public sealed record AbCipDeviceOptions(
|
|||||||
/// GuardLogix controller; non-safety writes violate the safety-partition isolation and are
|
/// GuardLogix controller; non-safety writes violate the safety-partition isolation and are
|
||||||
/// rejected by the PLC anyway. Surfaces the intent explicitly instead of relying on the
|
/// rejected by the PLC anyway. Surfaces the intent explicitly instead of relying on the
|
||||||
/// write attempt failing at runtime.</param>
|
/// write attempt failing at runtime.</param>
|
||||||
|
/// <param name="StringLength">Capacity of the DATA character array on a Logix STRING / STRINGnn
|
||||||
|
/// UDT — 82 for the stock <c>STRING</c>, 20/40/80/etc for user-defined <c>STRING_20</c>,
|
||||||
|
/// <c>STRING_40</c>, <c>STRING_80</c> variants. Threads through libplctag's
|
||||||
|
/// <c>str_max_capacity</c> attribute so the wrapper allocates the correct backing buffer
|
||||||
|
/// and <c>GetString</c> / <c>SetString</c> truncate at the right boundary. <c>null</c>
|
||||||
|
/// keeps libplctag's default 82-byte STRING behaviour for back-compat. Ignored for
|
||||||
|
/// non-<see cref="AbCipDataType.String"/> types.</param>
|
||||||
public sealed record AbCipTagDefinition(
|
public sealed record AbCipTagDefinition(
|
||||||
string Name,
|
string Name,
|
||||||
string DeviceHostAddress,
|
string DeviceHostAddress,
|
||||||
@@ -100,7 +107,8 @@ public sealed record AbCipTagDefinition(
|
|||||||
bool Writable = true,
|
bool Writable = true,
|
||||||
bool WriteIdempotent = false,
|
bool WriteIdempotent = false,
|
||||||
IReadOnlyList<AbCipStructureMember>? Members = null,
|
IReadOnlyList<AbCipStructureMember>? Members = null,
|
||||||
bool SafetyTag = false);
|
bool SafetyTag = false,
|
||||||
|
int? StringLength = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. <c>Speed</c>,
|
/// One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. <c>Speed</c>,
|
||||||
@@ -112,7 +120,8 @@ public sealed record AbCipStructureMember(
|
|||||||
string Name,
|
string Name,
|
||||||
AbCipDataType DataType,
|
AbCipDataType DataType,
|
||||||
bool Writable = true,
|
bool Writable = true,
|
||||||
bool WriteIdempotent = false);
|
bool WriteIdempotent = false,
|
||||||
|
int? StringLength = null);
|
||||||
|
|
||||||
/// <summary>Which AB PLC family the device is — selects the profile applied to connection params.</summary>
|
/// <summary>Which AB PLC family the device is — selects the profile applied to connection params.</summary>
|
||||||
public enum AbCipPlcFamily
|
public enum AbCipPlcFamily
|
||||||
|
|||||||
@@ -65,10 +65,15 @@ public interface IAbCipTagFactory
|
|||||||
/// <param name="LibplctagPlcAttribute">libplctag <c>plc=...</c> attribute, per family profile.</param>
|
/// <param name="LibplctagPlcAttribute">libplctag <c>plc=...</c> attribute, per family profile.</param>
|
||||||
/// <param name="TagName">Logix symbolic tag name as emitted by <see cref="AbCipTagPath.ToLibplctagName"/>.</param>
|
/// <param name="TagName">Logix symbolic tag name as emitted by <see cref="AbCipTagPath.ToLibplctagName"/>.</param>
|
||||||
/// <param name="Timeout">libplctag operation timeout (applies to Initialize / Read / Write).</param>
|
/// <param name="Timeout">libplctag operation timeout (applies to Initialize / Read / Write).</param>
|
||||||
|
/// <param name="StringMaxCapacity">Optional Logix STRINGnn DATA-array capacity (e.g. 20 / 40 / 80
|
||||||
|
/// for <c>STRING_20</c> / <c>STRING_40</c> / <c>STRING_80</c> UDTs). Threads through libplctag's
|
||||||
|
/// <c>str_max_capacity</c> attribute. <c>null</c> keeps libplctag's default 82-byte STRING
|
||||||
|
/// behaviour for back-compat.</param>
|
||||||
public sealed record AbCipTagCreateParams(
|
public sealed record AbCipTagCreateParams(
|
||||||
string Gateway,
|
string Gateway,
|
||||||
int Port,
|
int Port,
|
||||||
string CipPath,
|
string CipPath,
|
||||||
string LibplctagPlcAttribute,
|
string LibplctagPlcAttribute,
|
||||||
string TagName,
|
string TagName,
|
||||||
TimeSpan Timeout);
|
TimeSpan Timeout,
|
||||||
|
int? StringMaxCapacity = null);
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
|||||||
Name = p.TagName,
|
Name = p.TagName,
|
||||||
Timeout = p.Timeout,
|
Timeout = p.Timeout,
|
||||||
};
|
};
|
||||||
|
// PR abcip-1.2 — Logix STRINGnn variant decoding. When the caller pins a non-default
|
||||||
|
// DATA-array capacity (STRING_20 / STRING_40 / STRING_80 etc.), forward it to libplctag
|
||||||
|
// via the StringMaxCapacity attribute so GetString / SetString truncate at the right
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
||||||
|
|||||||
@@ -211,4 +211,79 @@ public sealed class AbCipDriverReadTests
|
|||||||
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
|
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
|
||||||
factory.Tags["Nope"].Disposed.ShouldBeTrue();
|
factory.Tags["Nope"].Disposed.ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PR abcip-1.2 — STRINGnn variant decoding. Threading <see cref="AbCipTagDefinition.StringLength"/>
|
||||||
|
// through libplctag's StringMaxCapacity attribute lets STRING_20 / STRING_40 / STRING_80 UDTs
|
||||||
|
// decode against the right DATA-array size; null preserves the default 82-byte STRING.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StringLength_threads_into_TagCreateParams_StringMaxCapacity()
|
||||||
|
{
|
||||||
|
var (drv, factory) = NewDriver(
|
||||||
|
new AbCipTagDefinition("Banner", "ab://10.0.0.5/1,0", "Banner", AbCipDataType.String,
|
||||||
|
StringLength: 40));
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
factory.Customise = p => new FakeAbCipTag(p) { Value = "hello" };
|
||||||
|
|
||||||
|
await drv.ReadAsync(["Banner"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Banner"].CreationParams.StringMaxCapacity.ShouldBe(40);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StringLength_null_leaves_StringMaxCapacity_null_for_back_compat()
|
||||||
|
{
|
||||||
|
var (drv, factory) = NewDriver(
|
||||||
|
new AbCipTagDefinition("LegacyStr", "ab://10.0.0.5/1,0", "LegacyStr", AbCipDataType.String));
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
factory.Customise = p => new FakeAbCipTag(p) { Value = "world" };
|
||||||
|
|
||||||
|
await drv.ReadAsync(["LegacyStr"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["LegacyStr"].CreationParams.StringMaxCapacity.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StringLength_ignored_for_non_String_data_types()
|
||||||
|
{
|
||||||
|
// StringLength on a DINT-typed tag must not flow into StringMaxCapacity — libplctag would
|
||||||
|
// otherwise re-shape the buffer and corrupt the read. EnsureTagRuntimeAsync gates on the
|
||||||
|
// declared DataType.
|
||||||
|
var (drv, factory) = NewDriver(
|
||||||
|
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt,
|
||||||
|
StringLength: 80));
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
factory.Customise = p => new FakeAbCipTag(p) { Value = 7 };
|
||||||
|
|
||||||
|
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Speed"].CreationParams.StringMaxCapacity.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UDT_member_StringLength_threads_through_to_member_runtime()
|
||||||
|
{
|
||||||
|
// STRINGnn members of a UDT — declaration-driven fan-out copies StringLength from
|
||||||
|
// AbCipStructureMember onto the synthesised member AbCipTagDefinition; the per-member
|
||||||
|
// runtime then receives the right StringMaxCapacity.
|
||||||
|
var udt = new AbCipTagDefinition(
|
||||||
|
Name: "Recipe",
|
||||||
|
DeviceHostAddress: "ab://10.0.0.5/1,0",
|
||||||
|
TagPath: "Recipe",
|
||||||
|
DataType: AbCipDataType.Structure,
|
||||||
|
Members: [
|
||||||
|
new AbCipStructureMember("Name", AbCipDataType.String, StringLength: 20),
|
||||||
|
new AbCipStructureMember("Description", AbCipDataType.String, StringLength: 80),
|
||||||
|
new AbCipStructureMember("Code", AbCipDataType.DInt),
|
||||||
|
]);
|
||||||
|
var (drv, factory) = NewDriver(udt);
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
factory.Customise = p => new FakeAbCipTag(p) { Value = "x" };
|
||||||
|
|
||||||
|
await drv.ReadAsync(["Recipe.Name", "Recipe.Description", "Recipe.Code"], CancellationToken.None);
|
||||||
|
|
||||||
|
factory.Tags["Recipe.Name"].CreationParams.StringMaxCapacity.ShouldBe(20);
|
||||||
|
factory.Tags["Recipe.Description"].CreationParams.StringMaxCapacity.ShouldBe(80);
|
||||||
|
factory.Tags["Recipe.Code"].CreationParams.StringMaxCapacity.ShouldBeNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user