117 lines
4.6 KiB
C#
117 lines
4.6 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class MelsecAddressTests
|
|
{
|
|
// --- X / Y hex vs octal family trap ---
|
|
|
|
[Theory]
|
|
[InlineData("X0", (ushort)0)]
|
|
[InlineData("X9", (ushort)9)]
|
|
[InlineData("XA", (ushort)10)] // hex
|
|
[InlineData("XF", (ushort)15)]
|
|
[InlineData("X10", (ushort)16)] // hex 0x10 = decimal 16
|
|
[InlineData("X20", (ushort)32)] // hex 0x20 = decimal 32 — the classic MELSEC-Q trap
|
|
[InlineData("X1FF", (ushort)511)]
|
|
[InlineData("x10", (ushort)16)] // lowercase prefix
|
|
public void XInputToDiscrete_QLiQR_parses_hex(string x, ushort expected)
|
|
=> MelsecAddress.XInputToDiscrete(x, MelsecFamily.Q_L_iQR).ShouldBe(expected);
|
|
|
|
[Theory]
|
|
[InlineData("X0", (ushort)0)]
|
|
[InlineData("X7", (ushort)7)]
|
|
[InlineData("X10", (ushort)8)] // octal 10 = decimal 8
|
|
[InlineData("X20", (ushort)16)] // octal 20 = decimal 16 — SAME string, DIFFERENT value on FX
|
|
[InlineData("X777", (ushort)511)]
|
|
public void XInputToDiscrete_FiQF_parses_octal(string x, ushort expected)
|
|
=> MelsecAddress.XInputToDiscrete(x, MelsecFamily.F_iQF).ShouldBe(expected);
|
|
|
|
[Theory]
|
|
[InlineData("Y0", (ushort)0)]
|
|
[InlineData("Y1F", (ushort)31)]
|
|
public void YOutputToCoil_QLiQR_parses_hex(string y, ushort expected)
|
|
=> MelsecAddress.YOutputToCoil(y, MelsecFamily.Q_L_iQR).ShouldBe(expected);
|
|
|
|
[Theory]
|
|
[InlineData("Y0", (ushort)0)]
|
|
[InlineData("Y17", (ushort)15)]
|
|
public void YOutputToCoil_FiQF_parses_octal(string y, ushort expected)
|
|
=> MelsecAddress.YOutputToCoil(y, MelsecFamily.F_iQF).ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void Same_address_string_decodes_differently_between_families()
|
|
{
|
|
// This is the headline quirk: "X20" in GX Works means one thing on Q-series and
|
|
// another on FX-series. The driver's family selector is the only defence.
|
|
MelsecAddress.XInputToDiscrete("X20", MelsecFamily.Q_L_iQR).ShouldBe((ushort)32);
|
|
MelsecAddress.XInputToDiscrete("X20", MelsecFamily.F_iQF).ShouldBe((ushort)16);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("X8")] // 8 is non-octal
|
|
[InlineData("X12G")] // G is non-hex
|
|
public void XInputToDiscrete_FiQF_rejects_non_octal(string bad)
|
|
=> Should.Throw<ArgumentException>(() => MelsecAddress.XInputToDiscrete(bad, MelsecFamily.F_iQF));
|
|
|
|
[Theory]
|
|
[InlineData("X12G")]
|
|
public void XInputToDiscrete_QLiQR_rejects_non_hex(string bad)
|
|
=> Should.Throw<ArgumentException>(() => MelsecAddress.XInputToDiscrete(bad, MelsecFamily.Q_L_iQR));
|
|
|
|
[Fact]
|
|
public void XInputToDiscrete_honors_bank_base_from_assignment_block()
|
|
{
|
|
// Real-world QJ71MT91 assignment blocks commonly place X at DI 8192+ when other
|
|
// ranges take the low Modbus addresses. Helper must add the base cleanly.
|
|
MelsecAddress.XInputToDiscrete("X10", MelsecFamily.Q_L_iQR, xBankBase: 8192).ShouldBe((ushort)(8192 + 16));
|
|
}
|
|
|
|
// --- M-relay (decimal, both families) ---
|
|
|
|
[Theory]
|
|
[InlineData("M0", (ushort)0)]
|
|
[InlineData("M10", (ushort)10)] // M addresses are DECIMAL, not hex or octal
|
|
[InlineData("M511", (ushort)511)]
|
|
[InlineData("m99", (ushort)99)] // lowercase
|
|
public void MRelayToCoil_parses_decimal(string m, ushort expected)
|
|
=> MelsecAddress.MRelayToCoil(m).ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void MRelayToCoil_honors_bank_base()
|
|
=> MelsecAddress.MRelayToCoil("M0", mBankBase: 512).ShouldBe((ushort)512);
|
|
|
|
[Fact]
|
|
public void MRelayToCoil_rejects_non_numeric()
|
|
=> Should.Throw<ArgumentException>(() => MelsecAddress.MRelayToCoil("M1F"));
|
|
|
|
// --- D-register (decimal, both families) ---
|
|
|
|
[Theory]
|
|
[InlineData("D0", (ushort)0)]
|
|
[InlineData("D100", (ushort)100)]
|
|
[InlineData("d1023", (ushort)1023)]
|
|
public void DRegisterToHolding_parses_decimal(string d, ushort expected)
|
|
=> MelsecAddress.DRegisterToHolding(d).ShouldBe(expected);
|
|
|
|
[Fact]
|
|
public void DRegisterToHolding_honors_bank_base()
|
|
=> MelsecAddress.DRegisterToHolding("D10", dBankBase: 4096).ShouldBe((ushort)4106);
|
|
|
|
[Fact]
|
|
public void DRegisterToHolding_rejects_empty()
|
|
=> Should.Throw<ArgumentException>(() => MelsecAddress.DRegisterToHolding("D"));
|
|
|
|
// --- overflow ---
|
|
|
|
[Fact]
|
|
public void XInputToDiscrete_overflow_throws()
|
|
{
|
|
// 0xFFFF + base 1 = 0x10000 — past ushort.
|
|
Should.Throw<OverflowException>(() =>
|
|
MelsecAddress.XInputToDiscrete("XFFFF", MelsecFamily.Q_L_iQR, xBankBase: 1));
|
|
}
|
|
}
|