Merge pull request '[s7] S7 — LOGO!/S7-200 V-memory parser' (#340) from auto/s7/PR-S7-A5 into auto/driver-gaps
This commit was merged in pull request #340.
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
using S7NetCpuType = global::S7.Net.CpuType;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -31,7 +33,7 @@ public enum S7Size
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="Area">Memory area (DB, M, I, Q, T, C).</param>
|
/// <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>
|
/// <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
|
/// the offending input echoed in the message so operators can correlate to the tag
|
||||||
/// config that produced the fault.
|
/// config that produced the fault.
|
||||||
/// </summary>
|
/// </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))
|
if (string.IsNullOrWhiteSpace(address))
|
||||||
throw new FormatException("S7 address must not be empty");
|
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 'Q': return ParseMIQ(S7Area.Output, rest, address);
|
||||||
case 'T': return ParseTimerOrCounter(S7Area.Timer, rest, address);
|
case 'T': return ParseTimerOrCounter(S7Area.Timer, rest, address);
|
||||||
case 'C': return ParseTimerOrCounter(S7Area.Counter, rest, address);
|
case 'C': return ParseTimerOrCounter(S7Area.Counter, rest, address);
|
||||||
|
case 'V': return ParseV(rest, address, cpuType);
|
||||||
default:
|
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>
|
/// <summary>
|
||||||
/// Try-parse variant for callers that can't afford an exception on bad input (e.g.
|
/// 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
|
/// 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>
|
/// </summary>
|
||||||
public static bool TryParse(string address, out S7ParsedAddress result)
|
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
|
try
|
||||||
{
|
{
|
||||||
result = Parse(address);
|
result = Parse(address, cpuType);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (FormatException)
|
catch (FormatException)
|
||||||
@@ -206,6 +237,46 @@ public static class S7AddressParser
|
|||||||
return new S7ParsedAddress(area, DbNumber: 0, size, byteOffset, bitOffset);
|
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)
|
private static S7ParsedAddress ParseTimerOrCounter(S7Area area, string rest, string original)
|
||||||
{
|
{
|
||||||
if (rest.Length == 0)
|
if (rest.Length == 0)
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
|
|||||||
_parsedByName.Clear();
|
_parsedByName.Clear();
|
||||||
foreach (var t in _options.Tags)
|
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)
|
if (t.ElementCount is int n && n > 1)
|
||||||
{
|
{
|
||||||
// Array sanity: cap at S7 PDU realistic limit, reject variable-width
|
// Array sanity: cap at S7 PDU realistic limit, reject variable-width
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using S7NetCpuType = global::S7.Net.CpuType;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||||||
|
|
||||||
@@ -121,4 +122,45 @@ public sealed class S7AddressParserTests
|
|||||||
r.DbNumber.ShouldBe(1);
|
r.DbNumber.ShouldBe(1);
|
||||||
r.Size.ShouldBe(S7Size.Word);
|
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