diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
index 393f7ca..b7e6be2 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
@@ -26,8 +26,24 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
///
///
public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
- : IDriver, IDisposable, IAsyncDisposable
+ : IDriver, IReadable, IWritable, IDisposable, IAsyncDisposable
{
+ /// OPC UA StatusCode used when the tag name isn't in the driver's tag map.
+ private const uint StatusBadNodeIdUnknown = 0x80340000u;
+ /// OPC UA StatusCode used when the tag's data type isn't implemented yet.
+ private const uint StatusBadNotSupported = 0x803D0000u;
+ /// OPC UA StatusCode used when the tag is declared read-only.
+ private const uint StatusBadNotWritable = 0x803B0000u;
+ /// OPC UA StatusCode used when write fails validation (e.g. out-of-range value).
+ private const uint StatusBadInternalError = 0x80020000u;
+ /// OPC UA StatusCode used for socket / timeout / protocol-layer faults.
+ private const uint StatusBadCommunicationError = 0x80050000u;
+ /// OPC UA StatusCode used when S7 returns ErrorCode.WrongCPU / PUT/GET disabled.
+ private const uint StatusBadDeviceFailure = 0x80550000u;
+
+ private readonly Dictionary _tagsByName = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _parsedByName = new(StringComparer.OrdinalIgnoreCase);
+
private readonly S7DriverOptions _options = options;
private readonly SemaphoreSlim _gate = new(1, 1);
@@ -68,6 +84,19 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
await plc.OpenAsync(cts.Token).ConfigureAwait(false);
Plc = plc;
+
+ // Parse every tag's address once at init so config typos fail fast here instead
+ // of surfacing as BadInternalError on every Read against the bad tag. The parser
+ // also rejects bit-offset > 7, DB 0, unknown area letters, etc.
+ _tagsByName.Clear();
+ _parsedByName.Clear();
+ foreach (var t in _options.Tags)
+ {
+ var parsed = S7AddressParser.Parse(t.Address); // throws FormatException
+ _tagsByName[t.Name] = t;
+ _parsedByName[t.Name] = parsed;
+ }
+
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
}
catch (Exception ex)
@@ -106,6 +135,165 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+ // ---- IReadable ----
+
+ public async Task> ReadAsync(
+ IReadOnlyList fullReferences, CancellationToken cancellationToken)
+ {
+ var plc = RequirePlc();
+ var now = DateTime.UtcNow;
+ var results = new DataValueSnapshot[fullReferences.Count];
+
+ await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ for (var i = 0; i < fullReferences.Count; i++)
+ {
+ var name = fullReferences[i];
+ if (!_tagsByName.TryGetValue(name, out var tag))
+ {
+ results[i] = new DataValueSnapshot(null, StatusBadNodeIdUnknown, null, now);
+ continue;
+ }
+ try
+ {
+ 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);
+ }
+ }
+ }
+ finally { _gate.Release(); }
+ return results;
+ }
+
+ private async Task