diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusAddressParser.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusAddressParser.cs
new file mode 100644
index 0000000..477bb94
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusAddressParser.cs
@@ -0,0 +1,334 @@
+using System.Globalization;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
+
+///
+/// Parses the full Modbus tag-address grammar:
+/// <region><offset>[.<bit>][:<type>[<len>]][:<order>][:<count>].
+/// Output is a the driver-side config layer maps onto a
+/// ModbusTagDefinition.
+///
+///
+///
+/// The grammar mirrors industry conventions (Wonderware suffix style, Kepware/Modicon
+/// digit prefixes, Ignition mnemonic prefixes — all accepted) so users can paste tag
+/// spreadsheets from any of those tools without per-tag manual translation.
+///
+///
+/// Examples:
+///
+/// 40001 — HoldingRegisters[0], Int16 (default).
+/// 400001 — HoldingRegisters[0], Int16 (6-digit form).
+/// 40001.5 — bit 5 of HoldingRegisters[0].
+/// 40001:F — Float32 starting at HR[0] (consumes HR[0..1]).
+/// 40001:F:CDAB — same with word-swap byte order.
+/// 40001:STR20 — 20-char ASCII string.
+/// HR1:DI — Int32 at HR[0] using mnemonic region.
+/// 40001:F:5 — Float32[5] array (consumes HR[0..9]).
+/// 40001:I::10 — Int16[10] using default byte order (empty order field).
+/// C100 — Coils[99] (mnemonic).
+///
+///
+///
+public static class ModbusAddressParser
+{
+ /// Parse an address string. Throws on invalid input.
+ public static ParsedModbusAddress Parse(string address)
+ {
+ if (TryParse(address, out var parsed, out var error))
+ return parsed!;
+ throw new FormatException(error);
+ }
+
+ ///
+ /// Try-parse variant for config-bind paths that surface diagnostics rather than throw.
+ /// is null and non-null on failure.
+ ///
+ public static bool TryParse(string? address, out ParsedModbusAddress? result, out string? error)
+ {
+ result = null;
+
+ if (string.IsNullOrWhiteSpace(address))
+ {
+ error = "Modbus address is null or empty";
+ return false;
+ }
+
+ var s = address.Trim();
+
+ // Split on ':' — the fields are: [.bit] :type :order :count.
+ // Empty fields (e.g. "40001:I::5") are allowed and mean "use default."
+ var parts = s.Split(':');
+ if (parts.Length > 4)
+ {
+ error = $"Modbus address has too many ':'-separated fields ({parts.Length} > 4): '{address}'";
+ return false;
+ }
+
+ var addressPart = parts[0];
+ var typePart = parts.Length > 1 ? parts[1] : null;
+ string? orderPart = null;
+ string? countPart = null;
+
+ // 3-field form is shorthand: ::. X is either a byte-order mnemonic
+ // (4 letters — ABCD/CDAB/BADC/DCBA) or an array count (digits). Disambiguate by shape
+ // so users can write 40001:F:5 for Float[5] without the awkward 40001:F::5. Anything
+ // else surfaces a clear error in whichever slot it lands.
+ if (parts.Length == 3)
+ {
+ if (LooksLikeByteOrderToken(parts[2])) orderPart = parts[2];
+ else if (parts[2].All(char.IsDigit)) countPart = parts[2];
+ else
+ {
+ error = $"3rd field '{parts[2]}' must be a 4-letter byte order (ABCD/CDAB/BADC/DCBA) or a positive integer array count in '{address}'";
+ return false;
+ }
+ }
+ else if (parts.Length == 4)
+ {
+ orderPart = parts[2];
+ countPart = parts[3];
+ }
+
+ if (!TryParseRegionAndOffset(addressPart, out var region, out var offset, out var bit, out error))
+ return false;
+
+ // Type field — defaults: Bool for Coils/DiscreteInputs, Int16 for InputRegisters/HoldingRegisters,
+ // BitInRegister when bit-suffix is present.
+ ModbusDataType dataType;
+ ushort stringLen = 0;
+
+ if (bit.HasValue)
+ {
+ // Bit suffix forces BitInRegister; explicit type would conflict.
+ if (!string.IsNullOrEmpty(typePart))
+ {
+ error = $"Bit suffix '.{bit.Value}' cannot combine with explicit type ':{typePart}' in '{address}'";
+ return false;
+ }
+ dataType = ModbusDataType.BitInRegister;
+ }
+ else if (string.IsNullOrEmpty(typePart))
+ {
+ dataType = region is ModbusRegion.Coils or ModbusRegion.DiscreteInputs
+ ? ModbusDataType.Bool
+ : ModbusDataType.Int16;
+ }
+ else
+ {
+ if (!TryParseType(typePart, out dataType, out stringLen, out error))
+ return false;
+ }
+
+ // Region/type compatibility check — Coils and DiscreteInputs only carry Bool semantics.
+ if (region is ModbusRegion.Coils or ModbusRegion.DiscreteInputs && dataType != ModbusDataType.Bool)
+ {
+ error = $"Region {region} only supports Bool-typed tags; got {dataType} in '{address}'";
+ return false;
+ }
+
+ // Order field — defaults to BigEndian; only meaningful for multi-register types.
+ var order = ModbusByteOrder.BigEndian;
+ if (!string.IsNullOrEmpty(orderPart))
+ {
+ if (!TryParseByteOrder(orderPart, out order, out error))
+ return false;
+ }
+
+ // Count field — array length. Bit + array is rejected.
+ int? arrayCount = null;
+ if (!string.IsNullOrEmpty(countPart))
+ {
+ if (bit.HasValue)
+ {
+ error = $"Bit suffix and array count cannot combine in '{address}'";
+ return false;
+ }
+ if (!int.TryParse(countPart, NumberStyles.None, CultureInfo.InvariantCulture, out var parsedCount) || parsedCount < 1)
+ {
+ error = $"Array count must be a positive integer; got '{countPart}' in '{address}'";
+ return false;
+ }
+ arrayCount = parsedCount;
+ }
+
+ result = new ParsedModbusAddress(region, offset, bit, dataType, stringLen, order, arrayCount);
+ error = null;
+ return true;
+ }
+
+ private static bool TryParseRegionAndOffset(string text, out ModbusRegion region, out ushort offset, out byte? bit, out string? error)
+ {
+ region = default;
+ offset = 0;
+ bit = null;
+
+ if (string.IsNullOrEmpty(text))
+ {
+ error = "Region/offset segment is empty";
+ return false;
+ }
+
+ // Optional bit suffix: '.N' at the end, N in 0..15. Strip before parsing region/offset.
+ var dotIdx = text.IndexOf('.');
+ var addrText = dotIdx < 0 ? text : text[..dotIdx];
+ if (dotIdx >= 0)
+ {
+ var bitText = text[(dotIdx + 1)..];
+ if (!byte.TryParse(bitText, NumberStyles.None, CultureInfo.InvariantCulture, out var bitVal) || bitVal > 15)
+ {
+ error = $"Bit index must be 0..15; got '{bitText}'";
+ return false;
+ }
+ bit = bitVal;
+ }
+
+ // Try mnemonic prefix first (HR, IR, C, DI). Cheaper than the digit branch and
+ // unambiguous when present. DI must be checked before D — we don't currently use D
+ // alone but stay defensive.
+ if (TryParseMnemonicAddress(addrText, out region, out offset, out error))
+ return true;
+
+ // Fall back to Modicon (5/6-digit). Reuses #136's parser.
+ if (ModbusModiconAddress.TryParse(addrText, out region, out offset, out error))
+ return true;
+
+ // Both branches failed; the Modicon error is the more specific diagnostic.
+ return false;
+ }
+
+ private static bool TryParseMnemonicAddress(string text, out ModbusRegion region, out ushort offset, out string? error)
+ {
+ region = default;
+ offset = 0;
+ error = null;
+
+ // Mnemonic = letter prefix + 1-based register number. We require pure-digit suffix
+ // after the prefix; anything else (including the Modicon-digit forms) falls through
+ // to the Modicon parser.
+ (string Prefix, ModbusRegion Region)[] candidates =
+ [
+ ("HR", ModbusRegion.HoldingRegisters),
+ ("IR", ModbusRegion.InputRegisters),
+ ("DI", ModbusRegion.DiscreteInputs),
+ ("C", ModbusRegion.Coils),
+ ];
+
+ foreach (var (prefix, mnemonicRegion) in candidates)
+ {
+ if (!text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
+ var rest = text[prefix.Length..];
+ if (rest.Length == 0 || !rest.All(char.IsDigit))
+ {
+ // Prefix matched but body is non-numeric — not a mnemonic address.
+ continue;
+ }
+ if (!int.TryParse(rest, NumberStyles.None, CultureInfo.InvariantCulture, out var n) || n < 1 || n > 65536)
+ {
+ error = $"Mnemonic register number must be 1..65536; got '{rest}'";
+ return false;
+ }
+ region = mnemonicRegion;
+ offset = (ushort)(n - 1);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryParseType(string text, out ModbusDataType type, out ushort stringLen, out string? error)
+ {
+ type = default;
+ stringLen = 0;
+ error = null;
+
+ // STR — string length glued to the type code.
+ if (text.StartsWith("STR", StringComparison.OrdinalIgnoreCase))
+ {
+ var lenText = text[3..];
+ if (lenText.Length == 0)
+ {
+ error = "STR type requires a length: STR";
+ return false;
+ }
+ if (!ushort.TryParse(lenText, NumberStyles.None, CultureInfo.InvariantCulture, out var len) || len < 1)
+ {
+ error = $"STR length must be a positive integer; got '{lenText}'";
+ return false;
+ }
+ type = ModbusDataType.String;
+ stringLen = len;
+ return true;
+ }
+
+ type = text.ToUpperInvariant() switch
+ {
+ "BOOL" => ModbusDataType.Bool,
+ "I" => ModbusDataType.Int16,
+ "UI" => ModbusDataType.UInt16,
+ "DI" or "L" => ModbusDataType.Int32,
+ "UDI" or "UL" => ModbusDataType.UInt32,
+ "LI" => ModbusDataType.Int64,
+ "ULI" => ModbusDataType.UInt64,
+ "F" => ModbusDataType.Float32,
+ "D" => ModbusDataType.Float64,
+ "BCD" => ModbusDataType.Bcd16,
+ "LBCD" => ModbusDataType.Bcd32,
+ _ => (ModbusDataType)(-1),
+ };
+
+ if ((int)type == -1)
+ {
+ error = $"Unknown type code '{text}'. Valid: BOOL, I, UI, DI, L, UDI, UL, LI, ULI, F, D, BCD, LBCD, STR";
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool LooksLikeByteOrderToken(string text) =>
+ text.Length == 4 && text.All(char.IsLetter);
+
+ private static bool TryParseByteOrder(string text, out ModbusByteOrder order, out string? error)
+ {
+ order = ModbusByteOrder.BigEndian;
+ error = null;
+
+ order = text.ToUpperInvariant() switch
+ {
+ "ABCD" => ModbusByteOrder.BigEndian,
+ "CDAB" => ModbusByteOrder.WordSwap,
+ "BADC" => ModbusByteOrder.ByteSwap,
+ "DCBA" => ModbusByteOrder.FullReverse,
+ _ => (ModbusByteOrder)(-1),
+ };
+
+ if ((int)order == -1)
+ {
+ error = $"Unknown byte order '{text}'. Valid: ABCD, CDAB, BADC, DCBA";
+ return false;
+ }
+
+ return true;
+ }
+}
+
+///
+/// Result of parsing a Modbus tag-address string. Maps directly onto the driver-side
+/// ModbusTagDefinition at config-bind time.
+///
+/// Coils / DiscreteInputs / InputRegisters / HoldingRegisters.
+/// Zero-based PDU offset.
+/// When non-null, the tag is a single-bit-in-register selector (0..15).
+/// Inferred from explicit type code or region default.
+/// Character count for ; zero otherwise.
+/// Word/byte ordering for multi-register types.
+/// Element count when the tag is an array; null for scalars.
+public sealed record ParsedModbusAddress(
+ ModbusRegion Region,
+ ushort Offset,
+ byte? Bit,
+ ModbusDataType DataType,
+ ushort StringLength,
+ ModbusByteOrder ByteOrder,
+ int? ArrayCount);
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusDataType.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusDataType.cs
new file mode 100644
index 0000000..2fce635
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusDataType.cs
@@ -0,0 +1,95 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
+
+///
+/// The set of value types a Modbus tag can decode to. Each type implies a fixed
+/// register-count: / / /
+/// / = 1 register; /
+/// / / = 2 registers;
+/// / / = 4 registers;
+/// = ceil(StringLength / 2) registers.
+///
+///
+/// Lives in the shared addressing assembly (alongside ) so the
+/// Admin UI and the parser can speak about value types without taking a transport-layer
+/// dependency on the wire driver.
+///
+public enum ModbusDataType
+{
+ Bool,
+ Int16,
+ UInt16,
+ Int32,
+ UInt32,
+ Int64,
+ UInt64,
+ Float32,
+ Float64,
+ /// Single bit within a holding register. BitIndex selects 0-15 LSB-first.
+ BitInRegister,
+ /// ASCII string packed 2 chars per register, StringLength characters long.
+ String,
+ ///
+ /// 16-bit binary-coded decimal. Each nibble encodes one decimal digit (0-9). Register
+ /// value 0x1234 decodes as decimal 1234 — NOT binary 0x04D2 = 4660.
+ /// DL205/DL260 and several Mitsubishi / Omron families store timers, counters, and
+ /// operator-facing numerics as BCD by default.
+ ///
+ Bcd16,
+ ///
+ /// 32-bit (two-register) BCD. Decodes 8 decimal digits. Word ordering follows the tag's
+ /// the same way does.
+ ///
+ Bcd32,
+}
+
+///
+/// Word/byte ordering for multi-register types. The four-letter mnemonic refers to the
+/// order in which bytes A, B, C, D appear on the wire when decoding a 4-byte value (e.g.
+/// a Float32) from two consecutive 16-bit registers.
+///
+///
+///
+/// (ABCD) is the Modbus-spec default: high word at lower
+/// address, big-endian within each register.
+///
+///
+/// (CDAB): keeps bytes big-endian within each register
+/// but swaps the word pair. Common on Siemens S7 over Modbus, Allen-Bradley, several
+/// Modicon families.
+///
+///
+/// (BADC): keeps the word pair in spec order but swaps
+/// bytes within each register. Encountered on a handful of legacy controllers exposing
+/// little-endian internals through Modbus.
+///
+///
+/// (DCBA): full byte reversal — equivalent to reading
+/// the value as little-endian. Some industrial PCs and gateways that bridge CAN /
+/// EtherNet/IP into Modbus surface their backplane order this way.
+///
+///
+/// For 8-byte (Int64 / UInt64 / Float64) values the same A/B/C/D semantics apply
+/// pairwise across the four registers; the implementation is straight-line.
+///
+///
+public enum ModbusByteOrder
+{
+ BigEndian,
+ WordSwap,
+ ByteSwap,
+ FullReverse,
+}
+
+///
+/// Per-register byte order for ASCII strings packed 2 chars per register. Standard Modbus
+/// convention is — the first character of each pair occupies
+/// the high byte of the register. AutomationDirect DirectLOGIC (DL205, DL260, DL350) and a
+/// handful of legacy controllers pack , which inverts that within
+/// each register. Word ordering across multiple registers is always ascending address for
+/// strings — only the byte order inside each register flips.
+///
+public enum ModbusStringByteOrder
+{
+ HighByteFirst,
+ LowByteFirst,
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs
index 2b17486..5a3979b 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs
@@ -117,8 +117,8 @@ public sealed class ModbusDriver
folder.Variable(t.Name, t.Name, new DriverAttributeInfo(
FullName: t.Name,
DriverDataType: MapDataType(t.DataType),
- IsArray: false,
- ArrayDim: null,
+ IsArray: t.ArrayCount.HasValue,
+ ArrayDim: t.ArrayCount.HasValue ? (uint)t.ArrayCount.Value : null,
SecurityClass: t.Writable ? SecurityClassification.Operate : SecurityClassification.ViewOnly,
IsHistorized: false,
IsAlarm: false,
@@ -166,40 +166,142 @@ public sealed class ModbusDriver
private async Task