Task #141 — Modbus subscribe-side knobs (deadband + write-on-change)
Two driver-side filters that ≥5 of 6 surveyed vendors expose: 1. Per-tag Deadband (double?, on ModbusTagDefinition) — when set, the PollGroupEngine onChange callback suppresses publishes whose distance from the last-published value is below the threshold. Reduces wire traffic to OPC UA clients on noisy analog signals (flow meters, temperatures). Numeric scalar types only — Bool / BitInRegister / String / array tags publish unconditionally. 2. WriteOnChangeOnly (bool, on ModbusDriverOptions) — when true, the driver short-circuits writes whose value matches the most recent successful write to that tag. Saves PLC bandwidth on clients that re-publish the same setpoint every scan. Cache invalidates on any read that returns a different value, so HMI-side changes don't get masked. Both default off so existing deployments see no behaviour change. Implementation: - ShouldPublish guard wraps the existing OnDataChange invocation. First sample always passes through (no baseline); subsequent samples compare via Convert.ToDouble for the cross-numeric-type math. - IsRedundantWrite check at the top of WriteAsync; on success the cache is populated. Object.Equals handles boxed-numeric equality; arrays are excluded (reference-equality would never match anyway). - ReadAsync invalidates the WriteOnChangeOnly cache when the new value differs from the cached last-written value. Tests (5 new ModbusSubscribeOptionsTests): - Deadband suppresses sub-threshold changes (100 → 102 → 106 → 107 with deadband=5 publishes 100 and 106 only). - Deadband=null still publishes every change. - WriteOnChangeOnly suppresses 3 identical 42 writes (only first hits wire). - WriteOnChangeOnly default false hits the wire every time. - Read-divergence cache invalidation: external panel write to 99, our client's re-write of 42 must NOT be suppressed. 220/220 unit tests green; existing ProtocolOptions tests hardened against probe-loop noise by disabling the probe in their fixtures.
This commit is contained in:
@@ -45,6 +45,7 @@ public static class ModbusDriverFactoryExtensions
|
||||
UseFC15ForSingleCoilWrites = dto.UseFC15ForSingleCoilWrites ?? false,
|
||||
UseFC16ForSingleRegisterWrites = dto.UseFC16ForSingleRegisterWrites ?? false,
|
||||
DisableFC23 = dto.DisableFC23 ?? false,
|
||||
WriteOnChangeOnly = dto.WriteOnChangeOnly ?? false,
|
||||
AutoReconnect = dto.AutoReconnect ?? true,
|
||||
Tags = dto.Tags is { Count: > 0 }
|
||||
? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
|
||||
@@ -102,7 +103,8 @@ public static class ModbusDriverFactoryExtensions
|
||||
? ModbusStringByteOrder.HighByteFirst
|
||||
: ParseEnum<ModbusStringByteOrder>(t.StringByteOrder, name, driverInstanceId, "StringByteOrder"),
|
||||
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||
ArrayCount: parsed.ArrayCount);
|
||||
ArrayCount: parsed.ArrayCount,
|
||||
Deadband: t.Deadband);
|
||||
}
|
||||
|
||||
return new ModbusTagDefinition(
|
||||
@@ -121,7 +123,8 @@ public static class ModbusDriverFactoryExtensions
|
||||
? ModbusStringByteOrder.HighByteFirst
|
||||
: ParseEnum<ModbusStringByteOrder>(t.StringByteOrder, t.Name, driverInstanceId, "StringByteOrder"),
|
||||
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||
ArrayCount: t.ArrayCount);
|
||||
ArrayCount: t.ArrayCount,
|
||||
Deadband: t.Deadband);
|
||||
}
|
||||
|
||||
private static T ParseEnum<T>(string? raw, string? tagName, string driverInstanceId, string field) where T : struct, Enum
|
||||
@@ -155,6 +158,7 @@ public static class ModbusDriverFactoryExtensions
|
||||
public bool? UseFC15ForSingleCoilWrites { get; init; }
|
||||
public bool? UseFC16ForSingleRegisterWrites { get; init; }
|
||||
public bool? DisableFC23 { get; init; }
|
||||
public bool? WriteOnChangeOnly { get; init; }
|
||||
public bool? AutoReconnect { get; init; }
|
||||
public List<ModbusTagDto>? Tags { get; init; }
|
||||
public ModbusProbeDto? Probe { get; init; }
|
||||
@@ -202,6 +206,7 @@ public static class ModbusDriverFactoryExtensions
|
||||
public string? StringByteOrder { get; init; }
|
||||
public bool? WriteIdempotent { get; init; }
|
||||
public int? ArrayCount { get; init; }
|
||||
public double? Deadband { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class ModbusProbeDto
|
||||
|
||||
Reference in New Issue
Block a user