Three substantive issues caught + fixed during the validation pass: 1. pymodbus rejects unknown keys at device-list / setup level. My PR 43 commit had `_layout_note`, `_uint16_layout`, `_bits_layout`, `_write_note` device-level JSON-comment fields that crashed pymodbus startup with `INVALID key in setup`. Removed all device-level _* fields. Inline `_quirk` keys WITHIN individual register entries are tolerated by pymodbus 3.13.0 — kept those in dl205.json since they document the byte math per quirk and the README + git history aren't enough context for a hand-author reading raw integer values. Documented the constraint in the top-level _comment of each profile. 2. pymodbus rejects sweeping `write` ranges that include any cell not assigned a type. My initial standard.json had `write: [[0, 2047]]` but only seeded HR[0..31] + HR[100] + HR[200..209] + bits[1024..1109] — pymodbus blew up on cell 32 (gap between HR[31] and HR[100]). Fixed by listing per-block write ranges that exactly mirror the seeded ranges. Same fix in dl205.json (was `[[0, 16383]]`). 3. pymodbus simulator stores all 4 standard Modbus tables in ONE underlying cell array — each cell can only be typed once (BITS or UINT16, not both). My initial standard.json had `bits[0..31]` AND `uint16[0..31]` overlapping at the same addresses; pymodbus crashed with `ERROR "uint16" <Cell> used`. Fixed by relocating coils to address 1024+, well clear of the uint16 entries at 0..209. Documented the layout constraint in the standard.json top-level _comment. Substantive driver bug fixed: ModbusTcpTransport.ConnectAsync was using `new TcpClient()` (default constructor — dual-stack, IPv6 first) then `ConnectAsync(host, port)` with the user's hostname. .NET's TcpClient default-resolves "localhost" to ::1 first, fails to connect to pymodbus (which binds 0.0.0.0 IPv4-only), and only then retries IPv4 — the failure surfaces as the entire ConnectAsync timeout (2s by default) before the IPv4 attempt even starts. PR 30's smoke test silently SKIPPED because the fixture's TCP probe hit the same dual-stack ordering and timed out. Both fixed: ModbusSimulatorFixture probe now resolves Dns.GetHostAddresses, prefers AddressFamily.InterNetwork, dials IPv4 explicitly. ModbusTcpTransport does the same — resolves first, prefers IPv4, falls back to whatever Dns returns (handles IPv6-only hosts in the future). This is a real production-readiness fix because most Modbus PLCs are IPv4-only — a generic dual-stack TcpClient would burn the entire connect timeout against any IPv4-only PLC, masquerading as a connection failure when the PLC is actually fine. Smoke-test address shifted HR[100] -> HR[200]. Standard.json's HR[100] is the auto-incrementing register that drives subscribe-and-receive tests, so write-then-read against it would race the increment. HR[200] is the first cell of a writable scratch range present in BOTH simulator profiles. DL205Profile.cs xml-doc updated to explain the shift; tag name "DL205_Smoke_HReg100" -> "Smoke_HReg200" + smoke test references updated. dl205.json gains a matching scratch HR[200..209] range so the smoke test runs identically against either profile. Validation matrix: - standard.json boot: clean (TCP 5020 listening within ~3s of pymodbus.simulator launch). - dl205.json boot: clean. - pymodbus client direct FC06 to HR[200]=1234 + FC03 read: round-trip OK. - raw-bytes PowerShell TcpClient FC06 + 12-byte response: matches FC06 spec (echo of address + value). - DL205SmokeTest against standard.json: 1/1 pass (was failing as 'BadInternalError' due to the dual-stack timeout + tag-name typo — both fixed). - DL205SmokeTest against dl205.json: 1/1 pass. - Modbus.Tests Unit suite: 52/52 pass — dual-stack transport fix is non-breaking. - Solution build clean. Memory + future-PR setup: pymodbus install + activation pattern is now bullet-pointed at the top of Pymodbus/README.md so future PRs (the per-quirk DL205_<behavior> tests in PR 44+) don't have to repeat the trial-and-error of getting the simulator + integration tests cooperating. The three bugs above are documented inline in the JSON profiles + ModbusTcpTransport so they don't bite again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
2.3 KiB
C#
50 lines
2.3 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
|
|
|
/// <summary>
|
|
/// Tag map for the AutomationDirect DL205 device class. Mirrors what the pymodbus
|
|
/// <c>dl205.json</c> profile in <c>Pymodbus/dl205.json</c> exposes (or the real PLC, when
|
|
/// <see cref="ModbusSimulatorFixture"/> is pointed at one).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is the scaffold — each tag is deliberately generic so the smoke test has stable
|
|
/// addresses to read. Device-specific quirk tests (word order, max-register, register-zero
|
|
/// access, etc.) will land in their own test classes alongside this profile as the user
|
|
/// validates each behavior in pymodbus; see <c>docs/v2/modbus-test-plan.md</c> §per-device
|
|
/// quirk catalog for the checklist.
|
|
/// </remarks>
|
|
public static class DL205Profile
|
|
{
|
|
/// <summary>
|
|
/// Holding register the smoke test writes + reads. Address 200 is the first cell of the
|
|
/// scratch HR range in both <c>Pymodbus/standard.json</c> (HR[200..209] = 0) and
|
|
/// <c>Pymodbus/dl205.json</c> (HR[4096..4103] added in PR 43 for the same purpose), so
|
|
/// the smoke test runs identically against either simulator profile. Originally
|
|
/// targeted HR[100] — moved to HR[200] when the standard profile claimed HR[100] as
|
|
/// the auto-incrementing register that drives subscribe-and-receive tests.
|
|
/// </summary>
|
|
public const ushort SmokeHoldingRegister = 200;
|
|
|
|
/// <summary>Value the smoke test writes then reads back to assert round-trip integrity.</summary>
|
|
public const short SmokeHoldingValue = 1234;
|
|
|
|
public static ModbusDriverOptions BuildOptions(string host, int port) => new()
|
|
{
|
|
Host = host,
|
|
Port = port,
|
|
UnitId = 1,
|
|
Timeout = TimeSpan.FromSeconds(2),
|
|
Tags =
|
|
[
|
|
new ModbusTagDefinition(
|
|
Name: "Smoke_HReg200",
|
|
Region: ModbusRegion.HoldingRegisters,
|
|
Address: SmokeHoldingRegister,
|
|
DataType: ModbusDataType.Int16,
|
|
Writable: true),
|
|
],
|
|
// Disable the background probe loop — integration tests drive reads explicitly and
|
|
// the probe would race with assertions.
|
|
Probe = new ModbusProbeOptions { Enabled = false },
|
|
};
|
|
}
|