Auto: s7-b1 — multi-variable PDU packing
Replaces the per-tag Plc.ReadAsync loop in S7Driver.ReadAsync with a batched ReadMultipleVarsAsync path. Scalar fixed-width tags (Bool, Byte, Int16/UInt16, Int32/UInt32, Float32, Float64) are bin-packed into ≤18-item batches at the default 240-byte PDU using S7.Net.Types.DataItem; arrays, strings, dates, 64-bit ints, and UDT-shaped types stay on the legacy ReadOneAsync path. On batch-level failure each tag in the batch falls back to ReadOneAsync so good tags still produce values and the offender gets its per-item StatusCode (BadDeviceFailure / BadCommunicationError). 100 scalar reads now coalesce into ≤6 PDU round-trips instead of 100. Closes #292
This commit is contained in:
@@ -205,6 +205,13 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Phase 1: classify each request into (a) unknown / not-found, (b) packable
|
||||
// scalar (Bool/Byte/Int16/UInt16/Int32/UInt32/Float32/Float64), or (c) needs
|
||||
// per-tag fallback (arrays, strings, dates, 64-bit ints, UDT-fanout). Packable
|
||||
// tags collect into 19-item batches sent via Plc.ReadMultipleVarsAsync; the
|
||||
// rest stay on the legacy ReadOneAsync path.
|
||||
var packableIndexes = new List<int>(fullReferences.Count);
|
||||
var fallbackIndexes = new List<int>();
|
||||
for (var i = 0; i < fullReferences.Count; i++)
|
||||
{
|
||||
var name = fullReferences[i];
|
||||
@@ -213,36 +220,135 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
|
||||
results[i] = new DataValueSnapshot(null, StatusBadNodeIdUnknown, null, now);
|
||||
continue;
|
||||
}
|
||||
try
|
||||
var addr = _parsedByName[name];
|
||||
if (S7ReadPacker.IsPackable(tag, addr)) packableIndexes.Add(i);
|
||||
else fallbackIndexes.Add(i);
|
||||
}
|
||||
|
||||
// Phase 2: bin-pack and dispatch the packable group via ReadMultipleVarsAsync.
|
||||
// On a per-batch S7.Net failure the whole batch falls back to ReadOneAsync per
|
||||
// tag — that way one bad item doesn't poison the rest of the batch and each
|
||||
// tag still gets its own per-item StatusCode (BadDeviceFailure for PUT/GET
|
||||
// refusal, BadCommunicationError for transport faults).
|
||||
if (packableIndexes.Count > 0)
|
||||
{
|
||||
var budget = S7ReadPacker.ItemBudget(S7ReadPacker.DefaultPduSize);
|
||||
var batches = S7ReadPacker.BinPack(packableIndexes, budget);
|
||||
foreach (var batch in batches)
|
||||
{
|
||||
var value = await ReadOneAsync(plc, tag, cancellationToken).ConfigureAwait(false);
|
||||
results[i] = new DataValueSnapshot(value, 0u, now, now);
|
||||
_health = new DriverHealth(DriverState.Healthy, now, null);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
results[i] = new DataValueSnapshot(null, StatusBadNotSupported, null, now);
|
||||
}
|
||||
catch (global::S7.Net.PlcException pex)
|
||||
{
|
||||
// S7.Net's PlcException carries an ErrorCode; PUT/GET-disabled on
|
||||
// S7-1200/1500 surfaces here. Map to BadDeviceFailure so operators see a
|
||||
// device-config problem (toggle PUT/GET in TIA Portal) rather than a
|
||||
// transient fault — per driver-specs.md §5.
|
||||
results[i] = new DataValueSnapshot(null, StatusBadDeviceFailure, null, now);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, pex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results[i] = new DataValueSnapshot(null, StatusBadCommunicationError, null, now);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
await ReadBatchAsync(plc, batch, fullReferences, results, now, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: per-tag fallback for everything that can't pack into a single
|
||||
// DataItem. Keeps the existing decode path as the source of truth for
|
||||
// string/date/array/64-bit semantics.
|
||||
foreach (var i in fallbackIndexes)
|
||||
{
|
||||
var tag = _tagsByName[fullReferences[i]];
|
||||
results[i] = await ReadOneAsSnapshotAsync(plc, tag, now, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read one packed batch via <c>Plc.ReadMultipleVarsAsync</c>. On batch
|
||||
/// success each <c>DataItem.Value</c> decodes into its tag's snapshot
|
||||
/// slot; on batch failure each tag in the batch falls back to
|
||||
/// <see cref="ReadOneAsSnapshotAsync"/> so the failure fans out per-tag instead
|
||||
/// of poisoning the whole batch with one StatusCode.
|
||||
/// </summary>
|
||||
private async Task ReadBatchAsync(
|
||||
global::S7.Net.Plc plc,
|
||||
IReadOnlyList<int> batchIndexes,
|
||||
IReadOnlyList<string> fullReferences,
|
||||
DataValueSnapshot[] results,
|
||||
DateTime now,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var items = new List<global::S7.Net.Types.DataItem>(batchIndexes.Count);
|
||||
foreach (var idx in batchIndexes)
|
||||
{
|
||||
var name = fullReferences[idx];
|
||||
items.Add(S7ReadPacker.BuildDataItem(_tagsByName[name], _parsedByName[name]));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var responses = await plc.ReadMultipleVarsAsync(items, ct).ConfigureAwait(false);
|
||||
// S7.Net mutates the input list in place and also returns it; iterate by
|
||||
// index against the input list so we are agnostic to either contract.
|
||||
for (var k = 0; k < batchIndexes.Count; k++)
|
||||
{
|
||||
var idx = batchIndexes[k];
|
||||
var tag = _tagsByName[fullReferences[idx]];
|
||||
var raw = (responses != null && k < responses.Count ? responses[k] : items[k]).Value;
|
||||
if (raw is null)
|
||||
{
|
||||
results[idx] = new DataValueSnapshot(null, StatusBadCommunicationError, null, now);
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
var decoded = S7ReadPacker.DecodePackedValue(tag, raw);
|
||||
results[idx] = new DataValueSnapshot(decoded, 0u, now, now);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results[idx] = new DataValueSnapshot(null, StatusBadInternalError, null, now);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
}
|
||||
}
|
||||
_health = new DriverHealth(DriverState.Healthy, now, null);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Batch-level fault: most likely a single bad address poisoned the
|
||||
// multi-var response. Fall back to ReadOneAsync per tag in the batch so
|
||||
// good tags still surface a value and the offender gets its own StatusCode.
|
||||
foreach (var idx in batchIndexes)
|
||||
{
|
||||
var tag = _tagsByName[fullReferences[idx]];
|
||||
results[idx] = await ReadOneAsSnapshotAsync(plc, tag, now, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single-tag read wrapped as a <see cref="DataValueSnapshot"/> with the same
|
||||
/// exception-to-StatusCode mapping the legacy per-tag loop applied. Shared
|
||||
/// between the fallback path and the post-batch retry path so the failure
|
||||
/// surface stays identical.
|
||||
/// </summary>
|
||||
private async Task<DataValueSnapshot> ReadOneAsSnapshotAsync(
|
||||
global::S7.Net.Plc plc, S7TagDefinition tag, DateTime now, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await ReadOneAsync(plc, tag, ct).ConfigureAwait(false);
|
||||
_health = new DriverHealth(DriverState.Healthy, now, null);
|
||||
return new DataValueSnapshot(value, 0u, now, now);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
return new DataValueSnapshot(null, StatusBadNotSupported, null, now);
|
||||
}
|
||||
catch (global::S7.Net.PlcException pex)
|
||||
{
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, pex.Message);
|
||||
return new DataValueSnapshot(null, StatusBadDeviceFailure, null, now);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
return new DataValueSnapshot(null, StatusBadCommunicationError, null, now);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<object> ReadOneAsync(global::S7.Net.Plc plc, S7TagDefinition tag, CancellationToken ct)
|
||||
{
|
||||
var addr = _parsedByName[tag.Name];
|
||||
|
||||
Reference in New Issue
Block a user