Auto: s7-a5 — LOGO!/S7-200 V-memory parser
Add CPU-aware overload S7AddressParser.Parse(string, CpuType?) that accepts the V area letter for S7-200 / S7-200 Smart / LOGO! 0BA8 and maps it to DataBlock DB1. V is rejected on S7-300/400/1200/1500 and on the legacy CPU-agnostic Parse(string) overload. Width suffixes mirror M/I/Q (VB/VW/VD/V0.0). S7Driver passes _options.CpuType so live tag config picks up family-aware parsing. Tests cover S7200/S7200Smart/Logo0BA8 positive cases, modern-family rejection, and CPU-agnostic rejection. Closes #291
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using S7NetCpuType = global::S7.Net.CpuType;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||
|
||||
/// <summary>
|
||||
@@ -31,7 +33,7 @@ public enum S7Size
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsed form of an S7 tag-address string. Produced by <see cref="S7AddressParser.Parse"/>.
|
||||
/// Parsed form of an S7 tag-address string. Produced by <see cref="S7AddressParser.Parse(string)"/>.
|
||||
/// </summary>
|
||||
/// <param name="Area">Memory area (DB, M, I, Q, T, C).</param>
|
||||
/// <param name="DbNumber">Data block number; only meaningful when <paramref name="Area"/> is <see cref="S7Area.DataBlock"/>.</param>
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
public static S7ParsedAddress Parse(string address)
|
||||
/// <remarks>
|
||||
/// The CPU-agnostic overload rejects the <c>V</c> area letter; <c>V</c> is only
|
||||
/// meaningful on S7-200 / S7-200 Smart / LOGO! where it maps to a fixed DB number
|
||||
/// (DB1 by convention) — call <see cref="Parse(string, S7NetCpuType?)"/> with the
|
||||
/// device's CPU family for V-memory tags.
|
||||
/// </remarks>
|
||||
public static S7ParsedAddress Parse(string address) => Parse(address, cpuType: null);
|
||||
|
||||
/// <summary>
|
||||
/// Parse an S7 address with knowledge of the device's CPU family. Required for the
|
||||
/// <c>V</c> 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
|
||||
/// <c>V</c> letter is rejected because it has no equivalent — those families use
|
||||
/// explicit <c>DB{n}.DB...</c> addressing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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 <see cref="VMemoryDbNumberFor"/> is the single point
|
||||
/// to extend. Live LOGO! testing is out of scope for the initial PR.
|
||||
/// </remarks>
|
||||
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)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try-parse variant for callers that can't afford an exception on bad input (e.g.
|
||||
/// config validation pages in the Admin UI). Returns <c>false</c> for any input that
|
||||
/// would throw from <see cref="Parse"/>.
|
||||
/// would throw from <see cref="Parse(string)"/>.
|
||||
/// </summary>
|
||||
public static bool TryParse(string address, out S7ParsedAddress result)
|
||||
=> TryParse(address, cpuType: null, out result);
|
||||
|
||||
/// <summary>
|
||||
/// Try-parse variant that accepts a CPU family for V-memory addressing.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a <c>V</c>-area address (S7-200 / S7-200 Smart / LOGO! V-memory). Same width
|
||||
/// suffixes as M/I/Q (<c>VB</c>, <c>VW</c>, <c>VD</c>, <c>V0.0</c>) 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.
|
||||
/// </summary>
|
||||
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 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>CpuType</c>
|
||||
/// enum surfaces). Throws for families that have no V-area concept.
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FormatException>(() => S7AddressParser.Parse("VW0", cpu));
|
||||
|
||||
[Fact]
|
||||
public void Parse_V_memory_rejected_when_no_CpuType_supplied()
|
||||
=> Should.Throw<FormatException>(() => S7AddressParser.Parse("VW0"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user