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"));
}