docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes
project bookkeeping IDs (task/tracking refs) from shipped code comments,
so the docs read cleanly and CommentChecker is quiet except for known
false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc).
Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
Joseph Doherty
2026-07-07 12:38:39 -04:00
parent 384dbd7d36
commit 9cad9ed0fc
375 changed files with 1899 additions and 2493 deletions
@@ -27,6 +27,7 @@ public static class DirectLogicAddress
/// V-address that may fall in either bank, call <see cref="VMemoryToPdu"/>, which routes
/// system-bank addresses through <see cref="SystemVMemoryToPdu"/> instead.
/// </remarks>
/// <returns>The 0-based Modbus PDU address for the given user V-memory address.</returns>
/// <exception cref="ArgumentException">Input is null / empty / contains non-octal digits (8,9).</exception>
/// <exception cref="OverflowException">Parsed value exceeds ushort.MaxValue (0xFFFF).</exception>
public static ushort UserVMemoryToPdu(string vAddress)
@@ -80,11 +81,16 @@ public static class DirectLogicAddress
/// <remarks>Octal 40400 == decimal 16640 (0x4100).</remarks>
public const ushort SystemVMemoryOctalBase = 0x4100; // octal 40400 decoded
/// <summary>
/// Convert a 0-based offset within the system V-memory bank to its absolute Modbus PDU
/// address, relocated to the fixed <see cref="SystemVMemoryBasePdu"/> base.
/// </summary>
/// <param name="offsetWithinSystemBank">
/// 0-based register offset within the system bank. Pass 0 for V40400 itself; pass 1 for
/// V40401 (octal), and so on. NOT an octal-decoded value — the system bank lives at
/// consecutive PDU addresses, so the offset is plain decimal.
/// </param>
/// <returns>The 0-based Modbus PDU address for the given system-bank offset.</returns>
public static ushort SystemVMemoryToPdu(ushort offsetWithinSystemBank)
{
var pdu = SystemVMemoryBasePdu + offsetWithinSystemBank;
@@ -107,6 +113,7 @@ public static class DirectLogicAddress
/// octal-decoded value itself: V40400 → PDU 0x2100, V40401 → 0x2101, and so on.
/// See <c>docs/v2/dl205.md</c> §V-Memory Addressing.
/// </remarks>
/// <returns>The 0-based Modbus PDU address for the given V-memory address.</returns>
/// <exception cref="ArgumentException">Input is null / empty / contains non-octal digits.</exception>
/// <exception cref="OverflowException">The result exceeds the 16-bit Modbus PDU range.</exception>
public static ushort VMemoryToPdu(string vAddress)
@@ -116,11 +123,11 @@ public static class DirectLogicAddress
return (ushort)octalValue;
// System bank: the registers are contiguous from V40400, so the offset within the bank
// is the plain decimal distance from the octal base, not another octal decode.
// Driver.Modbus.Addressing-011: the subtraction result is provably <= 0xBEFF because
// DecodeOctalVAddress already caps octalValue at 0xFFFF, so no overflow guard is needed
// here. The real overflow guard (pdu > ushort.MaxValue) lives in SystemVMemoryToPdu and
// is reachable only when that helper is called directly with a large explicit offset.
// is the plain decimal distance from the octal base, not another octal decode. The
// subtraction result is provably <= 0xBEFF because DecodeOctalVAddress already caps
// octalValue at 0xFFFF, so no overflow guard is needed here. The real overflow guard
// (pdu > ushort.MaxValue) lives in SystemVMemoryToPdu and is reachable only when that
// helper is called directly with a large explicit offset.
var offsetWithinBank = (ushort)(octalValue - SystemVMemoryOctalBase);
return SystemVMemoryToPdu(offsetWithinBank);
}
@@ -156,6 +163,7 @@ public static class DirectLogicAddress
/// ladder-logic editor's notation.
/// </summary>
/// <param name="yAddress">The Y-output address (octal, with optional Y prefix).</param>
/// <returns>The 0-based Modbus coil address.</returns>
public static ushort YOutputToCoil(string yAddress) =>
AddOctalOffset(YOutputBaseCoil, StripPrefix(yAddress, 'Y'));
@@ -164,6 +172,7 @@ public static class DirectLogicAddress
/// 0-based Modbus coil address.
/// </summary>
/// <param name="cAddress">The C-relay address (octal, with optional C prefix).</param>
/// <returns>The 0-based Modbus coil address.</returns>
public static ushort CRelayToCoil(string cAddress) =>
AddOctalOffset(CRelayBaseCoil, StripPrefix(cAddress, 'C'));
@@ -173,6 +182,7 @@ public static class DirectLogicAddress
/// exception — the CPU sizes the table to configured I/O, not installed modules.
/// </summary>
/// <param name="xAddress">The X-input address (octal, with optional X prefix).</param>
/// <returns>The 0-based Modbus discrete-input address.</returns>
public static ushort XInputToDiscrete(string xAddress) =>
AddOctalOffset(XInputBaseDiscrete, StripPrefix(xAddress, 'X'));
@@ -181,6 +191,7 @@ public static class DirectLogicAddress
/// Modbus discrete-input address. Accepts <c>"SP"</c> prefix case-insensitively.
/// </summary>
/// <param name="spAddress">The SP special-relay address (octal, with optional SP prefix).</param>
/// <returns>The 0-based Modbus discrete-input address.</returns>
public static ushort SpecialToDiscrete(string spAddress)
{
if (string.IsNullOrWhiteSpace(spAddress))
@@ -53,6 +53,7 @@ public static class MelsecAddress
/// 0-based Modbus DI address the assignment-block has configured X0 to land at.
/// Typical default on QJ71MT91 sample projects: 0. Pass the site-specific value.
/// </param>
/// <returns>The 0-based Modbus discrete-input address.</returns>
public static ushort XInputToDiscrete(string xAddress, MelsecFamily family, ushort xBankBase = 0) =>
AddFamilyOffset(xBankBase, StripPrefix(xAddress, 'X'), family);
@@ -63,6 +64,7 @@ public static class MelsecAddress
/// <param name="yAddress">MELSEC Y address. <c>Y</c> prefix optional, case-insensitive.</param>
/// <param name="family">The PLC family — determines whether the trailing digits are hex or octal.</param>
/// <param name="yBankBase">0-based Modbus coil address the assignment-block has configured Y0 to land at.</param>
/// <returns>The 0-based Modbus coil address.</returns>
public static ushort YOutputToCoil(string yAddress, MelsecFamily family, ushort yBankBase = 0) =>
AddFamilyOffset(yBankBase, StripPrefix(yAddress, 'Y'), family);
@@ -73,6 +75,7 @@ public static class MelsecAddress
/// </summary>
/// <param name="mAddress">MELSEC M address. <c>M</c> prefix optional, case-insensitive.</param>
/// <param name="mBankBase">0-based Modbus coil address the assignment-block has configured M0 to land at.</param>
/// <returns>The 0-based Modbus coil address.</returns>
public static ushort MRelayToCoil(string mAddress, ushort mBankBase = 0)
{
var digits = StripPrefix(mAddress, 'M');
@@ -93,6 +96,7 @@ public static class MelsecAddress
/// </summary>
/// <param name="dAddress">MELSEC D address. <c>D</c> prefix optional, case-insensitive.</param>
/// <param name="dBankBase">0-based Modbus holding register address the assignment-block has configured D0 to land at.</param>
/// <returns>The 0-based Modbus holding register address.</returns>
public static ushort DRegisterToHolding(string dAddress, ushort dBankBase = 0)
{
var digits = StripPrefix(dAddress, 'D');
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
/// spreadsheets from any of those tools without per-tag manual translation.
/// </para>
/// <para>
/// Examples (post-#146 type codes — verified against Wonderware DASMBTCP + Ignition):
/// Examples (type codes — verified against Wonderware DASMBTCP + Ignition):
/// <list type="bullet">
/// <item><c>40001</c> — HoldingRegisters[0], Int16 (default).</item>
/// <item><c>400001</c> — HoldingRegisters[0], Int16 (6-digit form).</item>
@@ -30,7 +30,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
/// </list>
/// </para>
/// <para>
/// <b>Grammar scope — out of band (Driver.Modbus.Addressing-007):</b> per-string byte
/// <b>Grammar scope — out of band:</b> per-string byte
/// order (<see cref="ModbusStringByteOrder"/>) is NOT expressible through this grammar.
/// The DL205 low-byte-first string-packing knob is configurable only via the structured
/// tag form (the driver's <c>ModbusTagDefinition.StringByteOrder</c> field). The 3rd
@@ -46,7 +46,7 @@ public static class ModbusAddressParser
/// <returns>The parsed Modbus address.</returns>
public static ParsedModbusAddress Parse(string address) => Parse(address, ModbusFamily.Generic, MelsecFamily.Q_L_iQR);
/// <summary>Parse with a family hint (#144 family-native branch).</summary>
/// <summary>Parse with a family hint (family-native branch).</summary>
/// <param name="address">The address string to parse.</param>
/// <param name="family">The Modbus family hint.</param>
/// <param name="melsecSubFamily">The MELSEC subfamily hint.</param>
@@ -113,7 +113,7 @@ public static class ModbusAddressParser
// else surfaces a clear error in whichever slot it lands.
if (parts.Length == 3)
{
// Driver.Modbus.Addressing-002: reject an empty 3rd field (e.g. "40001:F:") rather
// Reject an empty 3rd field (e.g. "40001:F:") rather
// than silently dropping it. Enumerable.All returns true for an empty sequence, so
// without this guard the empty string would be classified as a valid array count and
// then quietly ignored, leaving the user with no diagnostic for a typo'd trailing colon.
@@ -126,7 +126,7 @@ public static class ModbusAddressParser
else if (parts[2].All(char.IsDigit)) countPart = parts[2];
else
{
// Driver.Modbus.Addressing-003: when TryParseByteOrder would fail on a 4-letter
// When TryParseByteOrder would fail on a 4-letter
// token that looks like a type code (e.g. BOOL), improve the diagnostic so the
// user knows field 3 is a byte order and field 2 is the type.
var mightBeTypeCode = parts[2].Length == 4 && parts[2].All(char.IsLetterOrDigit);
@@ -223,7 +223,7 @@ public static class ModbusAddressParser
}
// Optional bit suffix: '.N' at the end, N in 0..15. Strip before parsing region/offset.
// Driver.Modbus.Addressing-004: use LastIndexOf so a multi-dot input like "40001.5.3"
// Use LastIndexOf so a multi-dot input like "40001.5.3"
// produces a descriptive error ("bit index '5.3' must be 0..15") rather than silently
// parsing "5" as the bit and leaving ".3" as part of the address text. Also validate
// the address segment is non-empty (a leading dot like ".5" is not a valid Modbus addr).
@@ -252,7 +252,7 @@ public static class ModbusAddressParser
bit = bitVal;
}
// Family-native branch (#144) — when a non-Generic family is configured, try its native
// Family-native branch — when a non-Generic family is configured, try its native
// syntax first. Successful native parse wins; failure falls through to Modicon / mnemonic.
// The order matters for cross-family ambiguity: DL205 'C100' is a control relay, not a
// Modicon coil, when the user has explicitly selected DL205.
@@ -271,11 +271,11 @@ public static class ModbusAddressParser
if (TryParseMnemonicAddress(addrText, out region, out offset, out error))
return true;
// Fall back to Modicon (5/6-digit). Reuses #136's parser.
// Fall back to Modicon (5/6-digit). Reuses the shared Modicon parser.
if (ModbusModiconAddress.TryParse(addrText, out region, out offset, out error))
return true;
// Driver.Modbus.Addressing-005: when a non-Generic family was configured and the
// When a non-Generic family was configured and the
// family-native parser set a specific error (meaning the address matched a recognised
// family prefix but the value was invalid, e.g. "contains non-octal digit"), prefer
// that error over the generic Modicon fallback diagnostic, which otherwise says
@@ -371,7 +371,7 @@ public static class ModbusAddressParser
}
catch (Exception ex)
{
// Driver.Modbus.Addressing-006: a try-parse method must never throw, so any helper
// A try-parse method must never throw, so any helper
// exception is converted to a structured error. The current helpers throw only
// ArgumentException (incl. ArgumentOutOfRangeException) and OverflowException, but
// catching narrowly would silently break the TryParse contract if a helper ever
@@ -447,17 +447,17 @@ public static class ModbusAddressParser
return true;
}
// #146 — codes aligned with Wonderware DASMBTCP + Ignition Modbus driver after the
// Codes aligned with Wonderware DASMBTCP + Ignition Modbus driver after the
// 2026-04-25 vendor-doc verification:
// - `:I` is Int32 (Wonderware: "letter 'I' follow ... 32-bit signed quantity, two
// consecutive registers"). Ignition's HRI is also Int32. The pre-#146 mapping
// consecutive registers"). Ignition's HRI is also Int32. The previous mapping
// `:I` = Int16 silently produced wrong-typed data + offset-shifted neighbours when
// a tag spreadsheet was pasted from another vendor.
// - `:S` is the explicit Int16 code (Wonderware: "letter 'S' ... 16-bit signed").
// - `:US` is UInt16 (Ignition: HRUS = "Unsigned Short").
// - `:UI` is UInt32 (parallel to `:I` shape; matches Ignition HRUI).
// - `:I_64` / `:UI_64` for 64-bit (Ignition HRI_64 / HRUI_64 underscore-N convention).
// - `:BCD_32` for 32-bit BCD (Ignition HRBCD_32). The pre-#146 `:LBCD` is dropped.
// - `:BCD_32` for 32-bit BCD (Ignition HRBCD_32). The previous `:LBCD` is dropped.
// - HR/IR with no explicit type still default to Int16 (matches Ignition `HR`).
type = text.ToUpperInvariant() switch
{
@@ -503,13 +503,12 @@ public static class ModbusAddressParser
if ((int)order == -1)
{
// Driver.Modbus.Addressing-003: if the unknown token is a VALID type code in this
// parser, direct the user to put it in field 2 instead. Only BOOL (4-letter) and
// STR<n> are valid type codes that could appear here. "REAL", "DINT", "UINT" look
// like type names from other tools (STEP 7 / RSLogix) but are NOT valid in this
// parser's type table (Driver.Modbus.Addressing-010) — listing them here would give
// advice that leads to a second error ("Unknown type code 'REAL'").
var upper = text.ToUpperInvariant();
// If the unknown token is a VALID type code in this parser, direct the user to put
// it in field 2 instead. Only BOOL (4-letter) and STR<n> are valid type codes that
// could appear here. "REAL", "DINT", "UINT" look like type names from other tools
// (STEP 7 / RSLogix) but are NOT valid in this parser's type table — listing them
// here would give advice that leads to a second error ("Unknown type code 'REAL'").
var isActualTypeCode = upper is "BOOL"
|| (text.Length > 3 && text.StartsWith("STR", StringComparison.OrdinalIgnoreCase)
&& text[3..].All(char.IsDigit));
@@ -90,7 +90,7 @@ public enum ModbusByteOrder
/// </summary>
/// <remarks>
/// <para>
/// <b>Grammar scope (Driver.Modbus.Addressing-007):</b> this enum is intentionally NOT
/// <b>Grammar scope:</b> this enum is intentionally NOT
/// expressible through the <see cref="ModbusAddressParser"/> grammar string. The grammar
/// has no token form for it, and <see cref="ParsedModbusAddress"/> has no field for it —
/// a DL205 string tag parsed from the grammar always carries the driver's default order.
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
/// <summary>
/// PLC family selector that drives the parser's family-native branch (#144). When the
/// PLC family selector that drives the parser's family-native branch. When the
/// driver is configured for a specific family, address strings using that family's native
/// notation (DirectLOGIC <c>V2000</c> octal, MELSEC <c>X20</c> hex/octal, etc.) are
/// translated to <see cref="ModbusRegion"/> + PDU offset directly — without forcing
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
/// <remarks>
/// <para>
/// When set to <see cref="Generic"/> (the default), the parser only accepts Modicon and
/// mnemonic forms — preserves pre-#144 behaviour exactly. Setting a non-Generic family
/// mnemonic forms — preserves the previous behaviour exactly. Setting a non-Generic family
/// is the only way to enable family-native parsing.
/// </para>
/// <para>
@@ -45,6 +45,7 @@ public static class ModbusModiconAddress
/// <param name="region">The parsed Modbus region, or default if parsing fails.</param>
/// <param name="offset">The zero-based PDU offset, or 0 if parsing fails.</param>
/// <param name="error">The error message if parsing fails, or null on success.</param>
/// <returns><c>true</c> if the address parsed successfully; otherwise <c>false</c>.</returns>
public static bool TryParse(string? address, out ModbusRegion region, out ushort offset, out string? error)
{
region = default;