Task #144 — Modbus family-native parser branch (DL205 / MELSEC)
Promotes DirectLogicAddress + MelsecAddress from "utility helpers an engineer calls manually" to "first-class branch of ModbusAddressParser." Users can now paste DL205-native (V2000, Y0, C100, X17, SP10) and MELSEC-native (D100, M50, X20 hex/octal, Y0) addresses directly into TagConfig and the parser handles the PLC-native → Modbus PDU translation. Changes: - Both helper files moved into the shared Driver.Modbus.Addressing assembly (same namespace, zero-churn for callers). Required because the parser needs to call them and the dependency direction is parser→helpers, not the other way. - New ModbusFamily enum (Generic / DL205 / MELSEC) on ModbusDriverOptions.Family. Generic preserves pre-#144 behaviour exactly. - ModbusDriverOptions.MelsecSubFamily picks the X/Y notation (Q_L_iQR hex vs F_iQF octal). Default Q_L_iQR. - ModbusAddressParser.Parse now takes optional family + sub-family hints. When non-Generic, family-native parsing runs FIRST; on miss falls back to Modicon / mnemonic. Cross-family ambiguity (C100 = Modicon coil under Generic, DL205 control relay under DL205) is unambiguous within one driver instance. - Suffix grammar composes with native addresses: V2000:F:CDAB:5 parses end-to-end as DL205 V-memory at PDU 1024 + Float32 + word-swap + array of 5. - Bit suffix composes too: V2000.7 parses as bit 7 of HR[1024]. - Factory DTO fields Family / MelsecSubFamily flow through to BuildTag so the JSON binding can drive everything per-driver. Tests: 16 new ModbusFamilyParserTests covering DL205 V/Y/C/X/SP, MELSEC D/M/X/Y, sub-family hex-vs-octal disambiguation, cross-family C100 ambiguity, fallback to Modicon when native misses, and grammar composition with bit/ byte-order/array modifiers. Existing 91 parser tests still green; 220 driver tests still green. Caveat: bank-base offsets for MELSEC X/Y/M default to 0 in the grammar string. Sites with non-zero "Modbus Device Assignment Parameter" bases must use the structured tag form to override — addressed in the docs refresh (#138).
This commit is contained in:
@@ -33,18 +33,27 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
public static class ModbusAddressParser
|
||||
{
|
||||
/// <summary>Parse an address string. Throws <see cref="FormatException"/> on invalid input.</summary>
|
||||
public static ParsedModbusAddress Parse(string address)
|
||||
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>
|
||||
public static ParsedModbusAddress Parse(string address, ModbusFamily family, MelsecFamily melsecSubFamily = MelsecFamily.Q_L_iQR)
|
||||
{
|
||||
if (TryParse(address, out var parsed, out var error))
|
||||
if (TryParse(address, family, melsecSubFamily, out var parsed, out var error))
|
||||
return parsed!;
|
||||
throw new FormatException(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try-parse variant for config-bind paths that surface diagnostics rather than throw.
|
||||
/// <paramref name="result"/> is null and <paramref name="error"/> non-null on failure.
|
||||
/// </summary>
|
||||
public static bool TryParse(string? address, out ParsedModbusAddress? result, out string? error)
|
||||
=> TryParse(address, ModbusFamily.Generic, MelsecFamily.Q_L_iQR, out result, out error);
|
||||
|
||||
/// <summary>
|
||||
/// Try-parse with a family hint. When <paramref name="family"/> is non-Generic, the
|
||||
/// parser tries the family-native form first (DL205 V-memory, MELSEC D-register, etc.)
|
||||
/// and falls back to Modicon / mnemonic on miss. <paramref name="result"/> is null and
|
||||
/// <paramref name="error"/> non-null on failure.
|
||||
/// </summary>
|
||||
public static bool TryParse(string? address, ModbusFamily family, MelsecFamily melsecSubFamily,
|
||||
out ParsedModbusAddress? result, out string? error)
|
||||
{
|
||||
result = null;
|
||||
|
||||
@@ -90,7 +99,7 @@ public static class ModbusAddressParser
|
||||
countPart = parts[3];
|
||||
}
|
||||
|
||||
if (!TryParseRegionAndOffset(addressPart, out var region, out var offset, out var bit, out error))
|
||||
if (!TryParseRegionAndOffset(addressPart, family, melsecSubFamily, out var region, out var offset, out var bit, out error))
|
||||
return false;
|
||||
|
||||
// Type field — defaults: Bool for Coils/DiscreteInputs, Int16 for InputRegisters/HoldingRegisters,
|
||||
@@ -157,7 +166,8 @@ public static class ModbusAddressParser
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseRegionAndOffset(string text, out ModbusRegion region, out ushort offset, out byte? bit, out string? error)
|
||||
private static bool TryParseRegionAndOffset(string text, ModbusFamily family, MelsecFamily melsecSubFamily,
|
||||
out ModbusRegion region, out ushort offset, out byte? bit, out string? error)
|
||||
{
|
||||
region = default;
|
||||
offset = 0;
|
||||
@@ -183,9 +193,15 @@ public static class ModbusAddressParser
|
||||
bit = bitVal;
|
||||
}
|
||||
|
||||
// Family-native branch (#144) — 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.
|
||||
if (family != ModbusFamily.Generic && TryParseFamilyNative(addrText, family, melsecSubFamily, out region, out offset, out error))
|
||||
return true;
|
||||
|
||||
// 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.
|
||||
// unambiguous when present.
|
||||
if (TryParseMnemonicAddress(addrText, out region, out offset, out error))
|
||||
return true;
|
||||
|
||||
@@ -197,6 +213,94 @@ public static class ModbusAddressParser
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseFamilyNative(string text, ModbusFamily family, MelsecFamily melsecSubFamily,
|
||||
out ModbusRegion region, out ushort offset, out string? error)
|
||||
{
|
||||
region = default;
|
||||
offset = 0;
|
||||
error = null;
|
||||
|
||||
try
|
||||
{
|
||||
switch (family)
|
||||
{
|
||||
case ModbusFamily.DL205:
|
||||
// V-memory → HoldingRegisters; Y → Coils; C → Coils (relays); X → DiscreteInputs;
|
||||
// SP → DiscreteInputs (special relays).
|
||||
if (text.StartsWith("V", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = DirectLogicAddress.UserVMemoryToPdu(text);
|
||||
region = ModbusRegion.HoldingRegisters;
|
||||
return true;
|
||||
}
|
||||
if (text.StartsWith("Y", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = DirectLogicAddress.YOutputToCoil(text);
|
||||
region = ModbusRegion.Coils;
|
||||
return true;
|
||||
}
|
||||
if (text.StartsWith("C", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = DirectLogicAddress.CRelayToCoil(text);
|
||||
region = ModbusRegion.Coils;
|
||||
return true;
|
||||
}
|
||||
if (text.StartsWith("X", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = DirectLogicAddress.XInputToDiscrete(text);
|
||||
region = ModbusRegion.DiscreteInputs;
|
||||
return true;
|
||||
}
|
||||
if (text.StartsWith("SP", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = DirectLogicAddress.SpecialToDiscrete(text);
|
||||
region = ModbusRegion.DiscreteInputs;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case ModbusFamily.MELSEC:
|
||||
// D-registers → HoldingRegisters; X → DiscreteInputs; Y → Coils; M → Coils.
|
||||
// The MelsecAddress helpers honour the sub-family (Q hex vs F octal) and use
|
||||
// bank base 0; users with non-zero assignment bases must use the structured
|
||||
// tag form to override. The grammar string covers the common base-0 path.
|
||||
if (text.StartsWith("D", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = MelsecAddress.DRegisterToHolding(text);
|
||||
region = ModbusRegion.HoldingRegisters;
|
||||
return true;
|
||||
}
|
||||
if (text.StartsWith("X", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = MelsecAddress.XInputToDiscrete(text, melsecSubFamily);
|
||||
region = ModbusRegion.DiscreteInputs;
|
||||
return true;
|
||||
}
|
||||
if (text.StartsWith("Y", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = MelsecAddress.YOutputToCoil(text, melsecSubFamily);
|
||||
region = ModbusRegion.Coils;
|
||||
return true;
|
||||
}
|
||||
if (text.StartsWith("M", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = MelsecAddress.MRelayToCoil(text);
|
||||
region = ModbusRegion.Coils;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or OverflowException)
|
||||
{
|
||||
error = $"Family-native parse for {family} failed on '{text}': {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseMnemonicAddress(string text, out ModbusRegion region, out ushort offset, out string? error)
|
||||
{
|
||||
region = default;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
|
||||
/// <summary>
|
||||
/// PLC family selector that drives the parser's family-native branch (#144). 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
|
||||
/// integration engineers to pre-translate to Modicon notation.
|
||||
/// </summary>
|
||||
/// <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
|
||||
/// is the only way to enable family-native parsing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Cross-family ambiguity</b>: <c>C100</c> means coil 100 under
|
||||
/// <see cref="Generic"/>, but DL260 control-relay 100 under <see cref="DL205"/>, etc.
|
||||
/// Per-driver Family selection makes the choice unambiguous within one driver instance;
|
||||
/// users with mixed families need separate driver instances per device.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public enum ModbusFamily
|
||||
{
|
||||
/// <summary>Default — only Modicon (4xxxx) and mnemonic (HR1, C100) forms are accepted.</summary>
|
||||
Generic,
|
||||
|
||||
/// <summary>
|
||||
/// AutomationDirect DirectLOGIC (DL205 / DL260 / DL350). V-memory is octal; Y / C
|
||||
/// are coils with hard-wired bank bases; X / SP are discrete inputs.
|
||||
/// </summary>
|
||||
DL205,
|
||||
|
||||
/// <summary>
|
||||
/// Mitsubishi MELSEC. X / Y interpretation depends on sub-family selection — see
|
||||
/// <see cref="MelsecFamily"/>. Defaults to Q/L/iQR (hex) when this family is selected.
|
||||
/// </summary>
|
||||
MELSEC,
|
||||
}
|
||||
@@ -46,9 +46,18 @@ public static class ModbusDriverFactoryExtensions
|
||||
UseFC16ForSingleRegisterWrites = dto.UseFC16ForSingleRegisterWrites ?? false,
|
||||
DisableFC23 = dto.DisableFC23 ?? false,
|
||||
WriteOnChangeOnly = dto.WriteOnChangeOnly ?? false,
|
||||
Family = dto.Family is null ? ModbusFamily.Generic
|
||||
: ParseEnum<ModbusFamily>(dto.Family, "<driver-level>", driverInstanceId, "Family"),
|
||||
MelsecSubFamily = dto.MelsecSubFamily is null ? MelsecFamily.Q_L_iQR
|
||||
: ParseEnum<MelsecFamily>(dto.MelsecSubFamily, "<driver-level>", driverInstanceId, "MelsecSubFamily"),
|
||||
AutoReconnect = dto.AutoReconnect ?? true,
|
||||
Tags = dto.Tags is { Count: > 0 }
|
||||
? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
|
||||
? [.. dto.Tags.Select(t => BuildTag(
|
||||
t, driverInstanceId,
|
||||
dto.Family is null ? ModbusFamily.Generic
|
||||
: ParseEnum<ModbusFamily>(dto.Family, "<driver-level>", driverInstanceId, "Family"),
|
||||
dto.MelsecSubFamily is null ? MelsecFamily.Q_L_iQR
|
||||
: ParseEnum<MelsecFamily>(dto.MelsecSubFamily, "<driver-level>", driverInstanceId, "MelsecSubFamily")))]
|
||||
: [],
|
||||
Probe = new ModbusProbeOptions
|
||||
{
|
||||
@@ -77,6 +86,9 @@ public static class ModbusDriverFactoryExtensions
|
||||
}
|
||||
|
||||
private static ModbusTagDefinition BuildTag(ModbusTagDto t, string driverInstanceId)
|
||||
=> BuildTag(t, driverInstanceId, ModbusFamily.Generic, MelsecFamily.Q_L_iQR);
|
||||
|
||||
private static ModbusTagDefinition BuildTag(ModbusTagDto t, string driverInstanceId, ModbusFamily family, MelsecFamily melsecSubFamily)
|
||||
{
|
||||
var name = t.Name ?? throw new InvalidOperationException(
|
||||
$"Modbus config for '{driverInstanceId}' has a tag missing Name");
|
||||
@@ -87,7 +99,7 @@ public static class ModbusDriverFactoryExtensions
|
||||
// from the grammar (Writable, WriteIdempotent, StringByteOrder) always come from the DTO.
|
||||
if (!string.IsNullOrWhiteSpace(t.AddressString))
|
||||
{
|
||||
if (!ModbusAddressParser.TryParse(t.AddressString, out var parsed, out var parseError))
|
||||
if (!ModbusAddressParser.TryParse(t.AddressString, family, melsecSubFamily, out var parsed, out var parseError))
|
||||
throw new InvalidOperationException(
|
||||
$"Modbus tag '{name}' in '{driverInstanceId}' has invalid AddressString '{t.AddressString}': {parseError}");
|
||||
return new ModbusTagDefinition(
|
||||
@@ -159,6 +171,8 @@ public static class ModbusDriverFactoryExtensions
|
||||
public bool? UseFC16ForSingleRegisterWrites { get; init; }
|
||||
public bool? DisableFC23 { get; init; }
|
||||
public bool? WriteOnChangeOnly { get; init; }
|
||||
public string? Family { get; init; }
|
||||
public string? MelsecSubFamily { get; init; }
|
||||
public bool? AutoReconnect { get; init; }
|
||||
public List<ModbusTagDto>? Tags { get; init; }
|
||||
public ModbusProbeDto? Probe { get; init; }
|
||||
|
||||
@@ -79,6 +79,20 @@ public sealed class ModbusDriverOptions
|
||||
/// </summary>
|
||||
public bool DisableFC23 { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// PLC family hint that drives the parser's family-native branch (#144). When set to a
|
||||
/// non-Generic value, address strings using that family's native syntax (DL205 V2000 /
|
||||
/// MELSEC D100) parse to the right region + offset directly. Defaults to
|
||||
/// <see cref="ModbusFamily.Generic"/> = Modicon-only behaviour preserved from #137.
|
||||
/// </summary>
|
||||
public ModbusFamily Family { get; init; } = ModbusFamily.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// MELSEC sub-family selector — only consulted when <see cref="Family"/> = MELSEC.
|
||||
/// Default Q/L/iQR (hex X/Y interpretation). Set F_iQF for FX-series PLCs.
|
||||
/// </summary>
|
||||
public MelsecFamily MelsecSubFamily { get; init; } = MelsecFamily.Q_L_iQR;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, the driver suppresses redundant writes: if the most recent
|
||||
/// successful write to a tag carried value V and a new write of V arrives, the second
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #144 family-native parser branch — DL205 + MELSEC. Family flag drives the parser to
|
||||
/// try the family's native syntax (V2000, D100, X20 hex/octal) before falling back to
|
||||
/// Modicon / mnemonic.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ModbusFamilyParserTests
|
||||
{
|
||||
// ----- DL205 native: V-memory (octal), Y/C/X/SP coils + discrete -----
|
||||
|
||||
[Theory]
|
||||
[InlineData("V0", 0)]
|
||||
[InlineData("V2000", 1024)] // octal 2000 = decimal 1024
|
||||
[InlineData("V40400", 16640)] // octal 40400 = decimal 16640 (system bank in user mapping per the helper)
|
||||
public void DL205_VMemory_To_HoldingRegisters(string addr, int expectedOffset)
|
||||
{
|
||||
var p = ModbusAddressParser.Parse(addr, ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
|
||||
p.Offset.ShouldBe((ushort)expectedOffset);
|
||||
p.DataType.ShouldBe(ModbusDataType.Int16);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DL205_Y_Output_Maps_To_Coils_Bank()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("Y0", ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.Coils);
|
||||
p.Offset.ShouldBe((ushort)2048); // YOutputBaseCoil
|
||||
p.DataType.ShouldBe(ModbusDataType.Bool);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DL205_C_Relay_Maps_To_Coils_Bank_NotModiconCoil()
|
||||
{
|
||||
// Cross-family ambiguity check: under Generic, "C100" is mnemonic Coils[99].
|
||||
// Under DL205 family, "C100" is a control relay → CRelayBaseCoil + octal(100) = 3072 + 64.
|
||||
var p = ModbusAddressParser.Parse("C100", ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.Coils);
|
||||
p.Offset.ShouldBe((ushort)(3072 + 64));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DL205_X_Input_Maps_To_DiscreteInputs()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("X17", ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
|
||||
p.Offset.ShouldBe((ushort)15); // octal 17 = decimal 15
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DL205_SP_Special_Relay_Maps_To_DiscreteInputs()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("SP10", ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
|
||||
p.Offset.ShouldBe((ushort)(1024 + 8)); // SpecialBaseDiscrete + octal(10)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DL205_Falls_Back_To_Modicon_When_Native_Misses()
|
||||
{
|
||||
// 40001 isn't a DL205 native form — falls through to the Modicon parser, returns
|
||||
// HoldingRegisters[0]. Important for users mixing legacy Modicon entries with native.
|
||||
var p = ModbusAddressParser.Parse("40001", ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
|
||||
p.Offset.ShouldBe((ushort)0);
|
||||
}
|
||||
|
||||
// ----- MELSEC native: D / X / Y / M with sub-family-aware X/Y parsing -----
|
||||
|
||||
[Fact]
|
||||
public void MELSEC_D_Register_Maps_To_HoldingRegisters()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("D100", ModbusFamily.MELSEC);
|
||||
p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
|
||||
p.Offset.ShouldBe((ushort)100); // base 0 + decimal 100
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MELSEC_M_Relay_Maps_To_Coils_Decimal()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("M50", ModbusFamily.MELSEC);
|
||||
p.Region.ShouldBe(ModbusRegion.Coils);
|
||||
p.Offset.ShouldBe((ushort)50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MELSEC_Q_Family_Treats_X20_As_Hex()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("X20", ModbusFamily.MELSEC, MelsecFamily.Q_L_iQR);
|
||||
p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
|
||||
p.Offset.ShouldBe((ushort)0x20); // hex 20 = decimal 32
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MELSEC_F_Family_Treats_X20_As_Octal()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("X20", ModbusFamily.MELSEC, MelsecFamily.F_iQF);
|
||||
p.Region.ShouldBe(ModbusRegion.DiscreteInputs);
|
||||
p.Offset.ShouldBe((ushort)16); // octal 20 = decimal 16
|
||||
}
|
||||
|
||||
// ----- Cross-family safety / Generic regression -----
|
||||
|
||||
[Fact]
|
||||
public void Generic_Family_Does_Not_Try_DL205_Branch()
|
||||
{
|
||||
// "V2000" under Generic isn't a known mnemonic OR a Modicon address → parse fails.
|
||||
// (Only DL205 / MELSEC families know V-memory.)
|
||||
ModbusAddressParser.TryParse("V2000", ModbusFamily.Generic, MelsecFamily.Q_L_iQR, out _, out var error)
|
||||
.ShouldBeFalse();
|
||||
error.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void C100_Under_Generic_Means_Modicon_Coil_99()
|
||||
{
|
||||
// Regression guard against the cross-family ambiguity: Generic must keep mnemonic "C"
|
||||
// mapping (Coil at offset = decimal-100 - 1).
|
||||
var p = ModbusAddressParser.Parse("C100", ModbusFamily.Generic);
|
||||
p.Region.ShouldBe(ModbusRegion.Coils);
|
||||
p.Offset.ShouldBe((ushort)99);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Suffix_Grammar_Composes_With_Native_Address()
|
||||
{
|
||||
// V2000:F:CDAB:5 should parse end-to-end: DL205 V2000 → HR[1024], Float32, word-swap, array of 5.
|
||||
var p = ModbusAddressParser.Parse("V2000:F:CDAB:5", ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
|
||||
p.Offset.ShouldBe((ushort)1024);
|
||||
p.DataType.ShouldBe(ModbusDataType.Float32);
|
||||
p.ByteOrder.ShouldBe(ModbusByteOrder.WordSwap);
|
||||
p.ArrayCount.ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DL205_Bit_Suffix_On_VMemory()
|
||||
{
|
||||
var p = ModbusAddressParser.Parse("V2000.7", ModbusFamily.DL205);
|
||||
p.Region.ShouldBe(ModbusRegion.HoldingRegisters);
|
||||
p.Offset.ShouldBe((ushort)1024);
|
||||
p.Bit.ShouldBe((byte)7);
|
||||
p.DataType.ShouldBe(ModbusDataType.BitInRegister);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user