@@ -35,6 +35,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private readonly Dictionary<string, AbCipTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly AbCipAlarmProjection _alarmProjection;
|
||||
private readonly SemaphoreSlim _discoverySemaphore = new(1, 1);
|
||||
private readonly AbCipWriteCoalescer _writeCoalescer = new();
|
||||
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
@@ -415,6 +416,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
_devices.Clear();
|
||||
_tagsByName.Clear();
|
||||
// PR abcip-4.2 — wipe the write-coalescer cache on shutdown. Reinitializing the driver
|
||||
// (Tier-B remediation) starts from a clean slate so the first write after restart pays
|
||||
// the full round-trip rather than reusing stale cached state.
|
||||
_writeCoalescer.ResetAll();
|
||||
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
||||
}
|
||||
|
||||
@@ -637,6 +642,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
state.HostState = newState;
|
||||
state.HostStateChangedUtc = DateTime.UtcNow;
|
||||
}
|
||||
// PR abcip-4.2 — drop the per-device write-coalescer cache when we lose the wire. The
|
||||
// PLC may have been restarted while we were offline + our cached "we already wrote 42"
|
||||
// is no longer valid PLC state. Reset on the Stopped transition (and again on the
|
||||
// recovery edge for safety) so the first post-reconnect write of any value pays the
|
||||
// full round-trip + the coalescer rebuilds its cache from the new baseline.
|
||||
if (newState == HostState.Stopped || newState == HostState.Running)
|
||||
_writeCoalescer.Reset(state.Options.HostAddress);
|
||||
OnHostStatusChanged?.Invoke(this,
|
||||
new HostStatusChangedEventArgs(state.Options.HostAddress, old, newState));
|
||||
}
|
||||
@@ -1255,6 +1267,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var def = entry.Definition;
|
||||
var w = entry.Request;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// PR abcip-4.2 — write deadband / write-on-change. Consult the coalescer first; a
|
||||
// suppression decision returns Good without hitting libplctag so the OPC UA client sees
|
||||
// the same write semantics it always has, the wire just doesn't move. Driver health is
|
||||
// intentionally left alone on suppression — a coalesced write is neither a success nor
|
||||
// a failure of the underlying connection. Bit-RMW writes go through their own path
|
||||
// (ExecuteBitRmwWriteAsync) which has its own coalescer call site.
|
||||
if (_writeCoalescer.ShouldSuppress(def.DeviceHostAddress, def, w.Value))
|
||||
return (entry.OriginalIndex, AbCipStatusMapper.Good);
|
||||
|
||||
try
|
||||
{
|
||||
var runtime = await EnsureTagRuntimeAsync(device, def, ct).ConfigureAwait(false);
|
||||
@@ -1265,6 +1287,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
if (status == 0)
|
||||
{
|
||||
_health = new DriverHealth(DriverState.Healthy, now, null);
|
||||
_writeCoalescer.Record(def.DeviceHostAddress, def, w.Value);
|
||||
return (entry.OriginalIndex, AbCipStatusMapper.Good);
|
||||
}
|
||||
return (entry.OriginalIndex, AbCipStatusMapper.MapLibplctagStatus(status));
|
||||
@@ -1309,13 +1332,24 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private async Task<uint> ExecuteBitRmwWriteAsync(
|
||||
DeviceState device, AbCipMultiWritePlanner.ClassifiedWrite entry, CancellationToken ct)
|
||||
{
|
||||
// PR abcip-4.2 — bit-RMW writes go through the coalescer too. The deadband path is
|
||||
// never useful on a single-bit BOOL (deadband < 1 collapses to equality) but
|
||||
// WriteOnChange is — a UI that toggles a SetPoint.Reset bit at every cycle benefits
|
||||
// from suppressing the redundant pulses.
|
||||
var def = entry.Definition;
|
||||
if (_writeCoalescer.ShouldSuppress(def.DeviceHostAddress, def, entry.Request.Value))
|
||||
return AbCipStatusMapper.Good;
|
||||
|
||||
try
|
||||
{
|
||||
var bit = entry.ParsedPath!.BitIndex!.Value;
|
||||
var code = await WriteBitInDIntAsync(device, entry.ParsedPath, bit, entry.Request.Value, ct)
|
||||
.ConfigureAwait(false);
|
||||
if (code == AbCipStatusMapper.Good)
|
||||
{
|
||||
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
|
||||
_writeCoalescer.Record(def.DeviceHostAddress, def, entry.Request.Value);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -1478,7 +1512,30 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
return runtime;
|
||||
}
|
||||
|
||||
public DriverHealth GetHealth() => _health;
|
||||
public DriverHealth GetHealth() => _health with { Diagnostics = BuildDiagnostics() };
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-4.2 — driver-attributable counter snapshot exposed via
|
||||
/// <see cref="DriverHealth.Diagnostics"/> + the <c>driver-diagnostics</c> RPC. Names use
|
||||
/// the <c>"<DriverType>.<Counter>"</c> convention so the Admin UI can render
|
||||
/// them alongside Modbus / S7 / OPC UA Client metrics without per-driver special-casing.
|
||||
/// Counters today: <c>AbCip.WritesSuppressed</c> (writes the coalescer skipped because
|
||||
/// deadband / write-on-change suppressed them) and <c>AbCip.WritesPassedThrough</c>
|
||||
/// (writes that hit the wire after consulting the coalescer). Future PRs add CIP-level
|
||||
/// counters (Forward Open count, multi-service-packet ratio, etc.) by extending this
|
||||
/// dictionary.
|
||||
/// </summary>
|
||||
private IReadOnlyDictionary<string, double> BuildDiagnostics() => new Dictionary<string, double>
|
||||
{
|
||||
["AbCip.WritesSuppressed"] = _writeCoalescer.TotalWritesSuppressed,
|
||||
["AbCip.WritesPassedThrough"] = _writeCoalescer.TotalWritesPassedThrough,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Test seam — exposes the live coalescer for unit tests that want to inspect counters
|
||||
/// without rebuilding the diagnostics dictionary on every assertion.
|
||||
/// </summary>
|
||||
internal AbCipWriteCoalescer WriteCoalescer => _writeCoalescer;
|
||||
|
||||
/// <summary>
|
||||
/// CLR-visible allocation footprint only — libplctag's native heap is invisible to the
|
||||
|
||||
Reference in New Issue
Block a user