chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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