diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7AddressParser.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7AddressParser.cs index be6641a..d35a047 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7AddressParser.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7AddressParser.cs @@ -1,3 +1,5 @@ +using S7NetCpuType = global::S7.Net.CpuType; + namespace ZB.MOM.WW.OtOpcUa.Driver.S7; /// @@ -31,7 +33,7 @@ public enum S7Size } /// -/// Parsed form of an S7 tag-address string. Produced by . +/// Parsed form of an S7 tag-address string. Produced by . /// /// Memory area (DB, M, I, Q, T, C). /// Data block number; only meaningful when is . @@ -74,7 +76,29 @@ public static class S7AddressParser /// the offending input echoed in the message so operators can correlate to the tag /// config that produced the fault. /// - public static S7ParsedAddress Parse(string address) + /// + /// The CPU-agnostic overload rejects the V area letter; V is only + /// meaningful on S7-200 / S7-200 Smart / LOGO! where it maps to a fixed DB number + /// (DB1 by convention) — call with the + /// device's CPU family for V-memory tags. + /// + public static S7ParsedAddress Parse(string address) => Parse(address, cpuType: null); + + /// + /// Parse an S7 address with knowledge of the device's CPU family. Required for the + /// V area letter (S7-200 / S7-200 Smart / LOGO! V-memory), which maps to + /// DataBlock DB1 on those families. On S7-300 / S7-400 / S7-1200 / S7-1500 the + /// V letter is rejected because it has no equivalent — those families use + /// explicit DB{n}.DB... addressing. + /// + /// + /// LOGO! firmware bands map V-memory to different underlying DB numbers in some + /// 0BA editions; the driver currently uses DB1 (the most common LOGO! 8 / 0BA8 + /// mapping). If a future site ships a firmware band where VM lives in a different + /// DB, the mapping table in is the single point + /// to extend. Live LOGO! testing is out of scope for the initial PR. + /// + public static S7ParsedAddress Parse(string address, S7NetCpuType? cpuType) { if (string.IsNullOrWhiteSpace(address)) throw new FormatException("S7 address must not be empty"); @@ -97,21 +121,28 @@ public static class S7AddressParser case 'Q': return ParseMIQ(S7Area.Output, rest, address); case 'T': return ParseTimerOrCounter(S7Area.Timer, rest, address); case 'C': return ParseTimerOrCounter(S7Area.Counter, rest, address); + case 'V': return ParseV(rest, address, cpuType); default: - throw new FormatException($"S7 address '{address}' starts with unknown area '{areaChar}' (expected DB/M/I/Q/T/C)"); + throw new FormatException($"S7 address '{address}' starts with unknown area '{areaChar}' (expected DB/M/I/Q/T/C/V)"); } } /// /// Try-parse variant for callers that can't afford an exception on bad input (e.g. /// config validation pages in the Admin UI). Returns false for any input that - /// would throw from . + /// would throw from . /// public static bool TryParse(string address, out S7ParsedAddress result) + => TryParse(address, cpuType: null, out result); + + /// + /// Try-parse variant that accepts a CPU family for V-memory addressing. + /// + public static bool TryParse(string address, S7NetCpuType? cpuType, out S7ParsedAddress result) { try { - result = Parse(address); + result = Parse(address, cpuType); return true; } catch (FormatException) @@ -206,6 +237,46 @@ public static class S7AddressParser return new S7ParsedAddress(area, DbNumber: 0, size, byteOffset, bitOffset); } + /// + /// Parse a V-area address (S7-200 / S7-200 Smart / LOGO! V-memory). Same width + /// suffixes as M/I/Q (VB, VW, VD, V0.0) but rewritten as + /// a DataBlock access so the rest of the driver — which speaks S7.Net's DB-centric + /// API — needs no special-casing downstream. + /// + private static S7ParsedAddress ParseV(string rest, string original, S7NetCpuType? cpuType) + { + var dbNumber = VMemoryDbNumberFor(cpuType, original); + // Reuse the M/I/Q grammar — V's size suffixes are identical (B/W/D/LD or .bit). + var parsed = ParseMIQ(S7Area.Memory, rest, original); + return parsed with { Area = S7Area.DataBlock, DbNumber = dbNumber }; + } + + /// + /// Map a CPU family to the underlying DB number that backs V-memory. Returns DB1 + /// for S7-200, S7-200 Smart, and LOGO! 0BA8 (the only LOGO! the S7.Net CpuType + /// enum surfaces). Throws for families that have no V-area concept. + /// + private static int VMemoryDbNumberFor(S7NetCpuType? cpuType, string original) + { + if (cpuType is null) + throw new FormatException( + $"S7 V-memory address '{original}' requires a CPU family (S7-200 / S7-200 Smart / LOGO!) — " + + "the CPU-agnostic Parse overload cannot resolve V-memory to a DB number"); + + return cpuType.Value switch + { + S7NetCpuType.S7200 => 1, + S7NetCpuType.S7200Smart => 1, + // LOGO! 8 / 0BA8 firmware bands typically expose VM as DB1 over S7comm. Older + // 0BA editions can differ; the mapping is centralised here for easy extension + // once a site provides a non-DB1 firmware band to test against. + S7NetCpuType.Logo0BA8 => 1, + _ => throw new FormatException( + $"S7 V-memory address '{original}' is only valid on S7-200 / S7-200 Smart / LOGO! " + + $"(got CpuType={cpuType.Value}); use explicit DB{{n}}.DB... addressing on this family"), + }; + } + private static S7ParsedAddress ParseTimerOrCounter(S7Area area, string rest, string original) { if (rest.Length == 0) diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs index 5c7cfa3..c1fa49d 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs @@ -102,7 +102,9 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId) _parsedByName.Clear(); foreach (var t in _options.Tags) { - var parsed = S7AddressParser.Parse(t.Address); // throws FormatException + // Pass CpuType so V-memory addresses (S7-200 / S7-200 Smart / LOGO!) resolve + // against the device's family-specific DB mapping. + var parsed = S7AddressParser.Parse(t.Address, _options.CpuType); // throws FormatException if (t.ElementCount is int n && n > 1) { // Array sanity: cap at S7 PDU realistic limit, reject variable-width diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7AddressParserTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7AddressParserTests.cs index cf5fed8..7a38ad8 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7AddressParserTests.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7AddressParserTests.cs @@ -1,5 +1,6 @@ using Shouldly; using Xunit; +using S7NetCpuType = global::S7.Net.CpuType; namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests; @@ -121,4 +122,45 @@ public sealed class S7AddressParserTests r.DbNumber.ShouldBe(1); r.Size.ShouldBe(S7Size.Word); } + + // --- V-memory (S7-200 / S7-200 Smart / LOGO!) --- + + [Theory] + [InlineData("VB0", S7Size.Byte, 0, 0)] + [InlineData("VW0", S7Size.Word, 0, 0)] + [InlineData("VD4", S7Size.DWord, 4, 0)] + [InlineData("V0.0", S7Size.Bit, 0, 0)] + [InlineData("V10.7", S7Size.Bit, 10, 7)] + public void Parse_V_memory_maps_to_DB1_for_S7200(string input, S7Size size, int byteOff, int bitOff) + { + var r = S7AddressParser.Parse(input, S7NetCpuType.S7200); + r.Area.ShouldBe(S7Area.DataBlock); + r.DbNumber.ShouldBe(1); + r.Size.ShouldBe(size); + r.ByteOffset.ShouldBe(byteOff); + r.BitOffset.ShouldBe(bitOff); + } + + [Theory] + [InlineData(S7NetCpuType.S7200Smart)] + [InlineData(S7NetCpuType.Logo0BA8)] + public void Parse_V_memory_maps_to_DB1_for_S7200Smart_and_LOGO(S7NetCpuType cpu) + { + var r = S7AddressParser.Parse("VW0", cpu); + r.Area.ShouldBe(S7Area.DataBlock); + r.DbNumber.ShouldBe(1); + r.Size.ShouldBe(S7Size.Word); + } + + [Theory] + [InlineData(S7NetCpuType.S71500)] + [InlineData(S7NetCpuType.S71200)] + [InlineData(S7NetCpuType.S7300)] + [InlineData(S7NetCpuType.S7400)] + public void Parse_V_memory_rejected_on_modern_families(S7NetCpuType cpu) + => Should.Throw(() => S7AddressParser.Parse("VW0", cpu)); + + [Fact] + public void Parse_V_memory_rejected_when_no_CpuType_supplied() + => Should.Throw(() => S7AddressParser.Parse("VW0")); }