From 02fccbc762cd3096844f1af9afaad60bf0112911 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 18 Apr 2026 21:14:02 -0400 Subject: [PATCH] =?UTF-8?q?Phase=203=20PR=2043=20=E2=80=94=20followup=20co?= =?UTF-8?q?mmit:=20validate=20pymodbus=20simulator=20end-to-end=20+=20fix?= =?UTF-8?q?=20three=20real=20bugs=20surfaced=20by=20running=20it.=20winget?= =?UTF-8?q?-installed=20Python=203.12.10=20+=20pip-installed=20pymodbus[si?= =?UTF-8?q?mulator]=3D=3D3.13.0=20on=20the=20dev=20box;=20both=20profiles?= =?UTF-8?q?=20boot=20cleanly,=20the=20integration-suite=20smoke=20test=20p?= =?UTF-8?q?asses=20against=20either=20profile.=20Three=20substantive=20iss?= =?UTF-8?q?ues=20caught=20+=20fixed=20during=20the=20validation=20pass:=20?= =?UTF-8?q?1.=20pymodbus=20rejects=20unknown=20keys=20at=20device-list=20/?= =?UTF-8?q?=20setup=20level.=20My=20PR=2043=20commit=20had=20`=5Flayout=5F?= =?UTF-8?q?note`,=20`=5Fuint16=5Flayout`,=20`=5Fbits=5Flayout`,=20`=5Fwrit?= =?UTF-8?q?e=5Fnote`=20device-level=20JSON-comment=20fields=20that=20crash?= =?UTF-8?q?ed=20pymodbus=20startup=20with=20`INVALID=20key=20in=20setup`.?= =?UTF-8?q?=20Removed=20all=20device-level=20=5F*=20fields.=20Inline=20`?= =?UTF-8?q?=5Fquirk`=20keys=20WITHIN=20individual=20register=20entries=20a?= =?UTF-8?q?re=20tolerated=20by=20pymodbus=203.13.0=20=E2=80=94=20kept=20th?= =?UTF-8?q?ose=20in=20dl205.json=20since=20they=20document=20the=20byte=20?= =?UTF-8?q?math=20per=20quirk=20and=20the=20README=20+=20git=20history=20a?= =?UTF-8?q?ren't=20enough=20context=20for=20a=20hand-author=20reading=20ra?= =?UTF-8?q?w=20integer=20values.=20Documented=20the=20constraint=20in=20th?= =?UTF-8?q?e=20top-level=20=5Fcomment=20of=20each=20profile.=202.=20pymodb?= =?UTF-8?q?us=20rejects=20sweeping=20`write`=20ranges=20that=20include=20a?= =?UTF-8?q?ny=20cell=20not=20assigned=20a=20type.=20My=20initial=20standar?= =?UTF-8?q?d.json=20had=20`write:=20[[0,=202047]]`=20but=20only=20seeded?= =?UTF-8?q?=20HR[0..31]=20+=20HR[100]=20+=20HR[200..209]=20+=20bits[1024..?= =?UTF-8?q?1109]=20=E2=80=94=20pymodbus=20blew=20up=20on=20cell=2032=20(ga?= =?UTF-8?q?p=20between=20HR[31]=20and=20HR[100]).=20Fixed=20by=20listing?= =?UTF-8?q?=20per-block=20write=20ranges=20that=20exactly=20mirror=20the?= =?UTF-8?q?=20seeded=20ranges.=20Same=20fix=20in=20dl205.json=20(was=20`[[?= =?UTF-8?q?0,=2016383]]`).=203.=20pymodbus=20simulator=20stores=20all=204?= =?UTF-8?q?=20standard=20Modbus=20tables=20in=20ONE=20underlying=20cell=20?= =?UTF-8?q?array=20=E2=80=94=20each=20cell=20can=20only=20be=20typed=20onc?= =?UTF-8?q?e=20(BITS=20or=20UINT16,=20not=20both).=20My=20initial=20standa?= =?UTF-8?q?rd.json=20had=20`bits[0..31]`=20AND=20`uint16[0..31]`=20overlap?= =?UTF-8?q?ping=20at=20the=20same=20addresses;=20pymodbus=20crashed=20with?= =?UTF-8?q?=20`ERROR=20"uint16"=20=20used`.=20Fixed=20by=20relocatin?= =?UTF-8?q?g=20coils=20to=20address=201024+,=20well=20clear=20of=20the=20u?= =?UTF-8?q?int16=20entries=20at=200..209.=20Documented=20the=20layout=20co?= =?UTF-8?q?nstraint=20in=20the=20standard.json=20top-level=20=5Fcomment.?= =?UTF-8?q?=20Substantive=20driver=20bug=20fixed:=20ModbusTcpTransport.Con?= =?UTF-8?q?nectAsync=20was=20using=20`new=20TcpClient()`=20(default=20cons?= =?UTF-8?q?tructor=20=E2=80=94=20dual-stack,=20IPv6=20first)=20then=20`Con?= =?UTF-8?q?nectAsync(host,=20port)`=20with=20the=20user's=20hostname.=20.N?= =?UTF-8?q?ET's=20TcpClient=20default-resolves=20"localhost"=20to=20::1=20?= =?UTF-8?q?first,=20fails=20to=20connect=20to=20pymodbus=20(which=20binds?= =?UTF-8?q?=200.0.0.0=20IPv4-only),=20and=20only=20then=20retries=20IPv4?= =?UTF-8?q?=20=E2=80=94=20the=20failure=20surfaces=20as=20the=20entire=20C?= =?UTF-8?q?onnectAsync=20timeout=20(2s=20by=20default)=20before=20the=20IP?= =?UTF-8?q?v4=20attempt=20even=20starts.=20PR=2030's=20smoke=20test=20sile?= =?UTF-8?q?ntly=20SKIPPED=20because=20the=20fixture's=20TCP=20probe=20hit?= =?UTF-8?q?=20the=20same=20dual-stack=20ordering=20and=20timed=20out.=20Bo?= =?UTF-8?q?th=20fixed:=20ModbusSimulatorFixture=20probe=20now=20resolves?= =?UTF-8?q?=20Dns.GetHostAddresses,=20prefers=20AddressFamily.InterNetwork?= =?UTF-8?q?,=20dials=20IPv4=20explicitly.=20ModbusTcpTransport=20does=20th?= =?UTF-8?q?e=20same=20=E2=80=94=20resolves=20first,=20prefers=20IPv4,=20fa?= =?UTF-8?q?lls=20back=20to=20whatever=20Dns=20returns=20(handles=20IPv6-on?= =?UTF-8?q?ly=20hosts=20in=20the=20future).=20This=20is=20a=20real=20produ?= =?UTF-8?q?ction-readiness=20fix=20because=20most=20Modbus=20PLCs=20are=20?= =?UTF-8?q?IPv4-only=20=E2=80=94=20a=20generic=20dual-stack=20TcpClient=20?= =?UTF-8?q?would=20burn=20the=20entire=20connect=20timeout=20against=20any?= =?UTF-8?q?=20IPv4-only=20PLC,=20masquerading=20as=20a=20connection=20fail?= =?UTF-8?q?ure=20when=20the=20PLC=20is=20actually=20fine.=20Smoke-test=20a?= =?UTF-8?q?ddress=20shifted=20HR[100]=20->=20HR[200].=20Standard.json's=20?= =?UTF-8?q?HR[100]=20is=20the=20auto-incrementing=20register=20that=20driv?= =?UTF-8?q?es=20subscribe-and-receive=20tests,=20so=20write-then-read=20ag?= =?UTF-8?q?ainst=20it=20would=20race=20the=20increment.=20HR[200]=20is=20t?= =?UTF-8?q?he=20first=20cell=20of=20a=20writable=20scratch=20range=20prese?= =?UTF-8?q?nt=20in=20BOTH=20simulator=20profiles.=20DL205Profile.cs=20xml-?= =?UTF-8?q?doc=20updated=20to=20explain=20the=20shift;=20tag=20name=20"DL2?= =?UTF-8?q?05=5FSmoke=5FHReg100"=20->=20"Smoke=5FHReg200"=20+=20smoke=20te?= =?UTF-8?q?st=20references=20updated.=20dl205.json=20gains=20a=20matching?= =?UTF-8?q?=20scratch=20HR[200..209]=20range=20so=20the=20smoke=20test=20r?= =?UTF-8?q?uns=20identically=20against=20either=20profile.=20Validation=20?= =?UTF-8?q?matrix:=20-=20standard.json=20boot:=20clean=20(TCP=205020=20lis?= =?UTF-8?q?tening=20within=20~3s=20of=20pymodbus.simulator=20launch).=20-?= =?UTF-8?q?=20dl205.json=20boot:=20clean.=20-=20pymodbus=20client=20direct?= =?UTF-8?q?=20FC06=20to=20HR[200]=3D1234=20+=20FC03=20read:=20round-trip?= =?UTF-8?q?=20OK.=20-=20raw-bytes=20PowerShell=20TcpClient=20FC06=20+=2012?= =?UTF-8?q?-byte=20response:=20matches=20FC06=20spec=20(echo=20of=20addres?= =?UTF-8?q?s=20+=20value).=20-=20DL205SmokeTest=20against=20standard.json:?= =?UTF-8?q?=201/1=20pass=20(was=20failing=20as=20'BadInternalError'=20due?= =?UTF-8?q?=20to=20the=20dual-stack=20timeout=20+=20tag-name=20typo=20?= =?UTF-8?q?=E2=80=94=20both=20fixed).=20-=20DL205SmokeTest=20against=20dl2?= =?UTF-8?q?05.json:=201/1=20pass.=20-=20Modbus.Tests=20Unit=20suite:=2052/?= =?UTF-8?q?52=20pass=20=E2=80=94=20dual-stack=20transport=20fix=20is=20non?= =?UTF-8?q?-breaking.=20-=20Solution=20build=20clean.=20Memory=20+=20futur?= =?UTF-8?q?e-PR=20setup:=20pymodbus=20install=20+=20activation=20pattern?= =?UTF-8?q?=20is=20now=20bullet-pointed=20at=20the=20top=20of=20Pymodbus/R?= =?UTF-8?q?EADME.md=20so=20future=20PRs=20(the=20per-quirk=20DL205=5F=20tests=20in=20PR=2044+)=20don't=20have=20to=20repeat=20t?= =?UTF-8?q?he=20trial-and-error=20of=20getting=20the=20simulator=20+=20int?= =?UTF-8?q?egration=20tests=20cooperating.=20The=20three=20bugs=20above=20?= =?UTF-8?q?are=20documented=20inline=20in=20the=20JSON=20profiles=20+=20Mo?= =?UTF-8?q?dbusTcpTransport=20so=20they=20don't=20bite=20again.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ModbusTcpTransport.cs | 14 +++- .../DL205/DL205Profile.cs | 18 +++-- .../DL205/DL205SmokeTests.cs | 4 +- .../ModbusSimulatorFixture.cs | 14 +++- .../Pymodbus/dl205.json | 40 ++++++++--- .../Pymodbus/standard.json | 70 ++++++++++++------- 6 files changed, 110 insertions(+), 50 deletions(-) diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs index 5e79e90..381e011 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs @@ -28,10 +28,20 @@ public sealed class ModbusTcpTransport : IModbusTransport public async Task ConnectAsync(CancellationToken ct) { - _client = new TcpClient(); + // Resolve the host explicitly + prefer IPv4. .NET's TcpClient default-constructor is + // dual-stack (IPv6 first, fallback to IPv4) — but most Modbus TCP devices (PLCs and + // simulators like pymodbus) bind 0.0.0.0 only, so the IPv6 attempt times out and we + // burn the entire ConnectAsync budget before even trying IPv4. Resolving first + + // dialing the IPv4 address directly sidesteps that. + var addresses = await System.Net.Dns.GetHostAddressesAsync(_host, ct).ConfigureAwait(false); + var ipv4 = System.Linq.Enumerable.FirstOrDefault(addresses, + a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); + var target = ipv4 ?? (addresses.Length > 0 ? addresses[0] : System.Net.IPAddress.Loopback); + + _client = new TcpClient(target.AddressFamily); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(_timeout); - await _client.ConnectAsync(_host, _port, cts.Token).ConfigureAwait(false); + await _client.ConnectAsync(target, _port, cts.Token).ConfigureAwait(false); _stream = _client.GetStream(); } diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs index 4b26a4c..8095a23 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs @@ -14,13 +14,17 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205; /// public static class DL205Profile { - /// Holding register the smoke test reads. Address 100 sidesteps the DL205 - /// register-zero quirk (pending confirmation) — see modbus-test-plan.md. - public const ushort SmokeHoldingRegister = 100; + /// + /// Holding register the smoke test writes + reads. Address 200 is the first cell of the + /// scratch HR range in both Pymodbus/standard.json (HR[200..209] = 0) and + /// Pymodbus/dl205.json (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. + /// + public const ushort SmokeHoldingRegister = 200; - /// Expected value the pymodbus profile seeds into register 100. When running - /// against a real DL205 (or a pymodbus profile where this register is writable), the smoke - /// test seeds this value first, then reads it back. + /// Value the smoke test writes then reads back to assert round-trip integrity. public const short SmokeHoldingValue = 1234; public static ModbusDriverOptions BuildOptions(string host, int port) => new() @@ -32,7 +36,7 @@ public static class DL205Profile Tags = [ new ModbusTagDefinition( - Name: "DL205_Smoke_HReg100", + Name: "Smoke_HReg200", Region: ModbusRegion.HoldingRegisters, Address: SmokeHoldingRegister, DataType: ModbusDataType.Int16, diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205SmokeTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205SmokeTests.cs index d1dd22c..69542a0 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205SmokeTests.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205SmokeTests.cs @@ -38,13 +38,13 @@ public sealed class DL205SmokeTests(ModbusSimulatorFixture sim) // zeroed at simulator start, and tests must not depend on prior-test state per the // test-plan conventions. var writeResults = await driver.WriteAsync( - [new(FullReference: "DL205_Smoke_HReg100", Value: (short)DL205Profile.SmokeHoldingValue)], + [new(FullReference: "Smoke_HReg200", Value: (short)DL205Profile.SmokeHoldingValue)], TestContext.Current.CancellationToken); writeResults.Count.ShouldBe(1); writeResults[0].StatusCode.ShouldBe(0u, "write must succeed against the ModbusPal DL205 profile"); var readResults = await driver.ReadAsync( - ["DL205_Smoke_HReg100"], + ["Smoke_HReg200"], TestContext.Current.CancellationToken); readResults.Count.ShouldBe(1); readResults[0].StatusCode.ShouldBe(0u); diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs index 7e34236..1996eff 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs @@ -46,8 +46,18 @@ public sealed class ModbusSimulatorFixture : IAsyncDisposable try { - using var client = new TcpClient(); - var task = client.ConnectAsync(Host, Port); + // Force IPv4 family on the probe — pymodbus's TCP server binds 0.0.0.0 (IPv4 only) + // while .NET's TcpClient default-resolves "localhost" → IPv6 ::1 first, fails to + // connect, and only then tries IPv4. Under .NET 10 the IPv6 fail surfaces as a + // 2s timeout (no graceful fallback by default), so the C# probe times out even + // though a PowerShell probe of the same endpoint succeeds. Resolving + dialing + // explicit IPv4 sidesteps the dual-stack ordering. + using var client = new TcpClient(System.Net.Sockets.AddressFamily.InterNetwork); + var task = client.ConnectAsync( + System.Net.Dns.GetHostAddresses(Host) + .FirstOrDefault(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + ?? System.Net.IPAddress.Loopback, + Port); if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected) { SkipReason = $"Modbus simulator at {Host}:{Port} did not accept a TCP connection within 2s. " + diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json index 0bd67cb..0686a49 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json @@ -1,5 +1,5 @@ { - "_comment": "DL205.json — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator. Models each behavior in docs/v2/dl205.md as concrete register values so DL205_ integration tests can assert against this profile WITHOUT a live PLC. Loaded by `pymodbus.simulator`. See ../README.md. Per-quirk address layout matches the table in dl205.md exactly. `shared blocks: true` matches DL series behavior — coils/HR overlay the same word address space (a Y-output is both a discrete bit AND part of a system V-memory register).", + "_comment": "DL205.json — DirectLOGIC DL205/DL260 quirk simulator. Models docs/v2/dl205.md as concrete register values. NOTE: pymodbus rejects unknown keys at device-list / setup level; explanatory comments live at top-level _comment + in README + git. Inline _quirk keys WITHIN individual register entries are accepted by pymodbus 3.13.0 (it only validates addr / value / action / parameters per entry). Each quirky uint16 is a pre-computed raw 16-bit value; pymodbus serves it verbatim. shared blocks=true matches DL series memory model. write list mirrors each seeded block — pymodbus rejects sweeping write ranges that include undefined cells.", "server_list": { "srv": { @@ -27,15 +27,37 @@ }, "invalid": [], "write": [ - [0, 16383] + [0, 0], + [200, 209], + [1024, 1024], + [1040, 1042], + [1056, 1057], + [1072, 1072], + [1280, 1282], + [1343, 1343], + [1407, 1407], + [2048, 2050], + [3072, 3074], + [4000, 4007], + [8448, 8448] ], - "_comment_uint16": "Holding-register seeds. Every quirky value is a raw uint16 with the byte math worked out in dl205.md so the simulator serves it verbatim — pymodbus does NOT decode strings, BCD, or float-CDAB on its own; that's the driver's job.", - "uint16": [ - {"_quirk": "V0 marker. HR[0] = 0xCAFE proves register 0 is valid on DL205/DL260 (rejects-register-0 was a DL05/DL06 relative-mode artefact). 0xCAFE = 51966.", + {"_quirk": "V0 marker. HR[0]=0xCAFE proves register 0 is valid on DL205/DL260 (rejects-register-0 was a DL05/DL06 relative-mode artefact). 0xCAFE = 51966.", "addr": 0, "value": 51966}, + {"_quirk": "Scratch HR range 200..209 — mirrors the standard.json scratch range so the smoke test (DL205Profile.SmokeHoldingRegister=200) round-trips identically against either profile.", + "addr": 200, "value": 0}, + {"addr": 201, "value": 0}, + {"addr": 202, "value": 0}, + {"addr": 203, "value": 0}, + {"addr": 204, "value": 0}, + {"addr": 205, "value": 0}, + {"addr": 206, "value": 0}, + {"addr": 207, "value": 0}, + {"addr": 208, "value": 0}, + {"addr": 209, "value": 0}, + {"_quirk": "V2000 marker. V2000 octal = decimal 1024 = PDU 0x0400. Marker 0x2000 = 8192.", "addr": 1024, "value": 8192}, @@ -57,16 +79,14 @@ {"_quirk": "BCD register. Decimal 1234 stored as BCD nibbles 0x1234 = 4660. NOT binary 1234 (= 0x04D2).", "addr": 1072, "value": 4660}, - {"_quirk": "FC03 cap test. Real DL205/DL260 FC03 caps at 128 registers (above spec's 125). HR[1280..1407] is 128 contiguous registers; rest of block defaults to 0.", + {"_quirk": "FC03 cap test marker — first cell of a 128-register span the FC03 cap test reads. Other cells in the span aren't seeded explicitly, so reads of HR[1283..1342] / 1344..1406 return the default 0; the seeded markers at 1280, 1281, 1282, 1343, 1407 prove the span boundaries.", "addr": 1280, "value": 0}, {"addr": 1281, "value": 1}, {"addr": 1282, "value": 2}, - {"addr": 1343, "value": 63, "_marker": "FC03Block_mid"}, - {"addr": 1407, "value": 127, "_marker": "FC03Block_last"} + {"addr": 1343, "value": 63}, + {"addr": 1407, "value": 127} ], - "_comment_bits": "Coils — Y outputs at 2048+, C relays at 3072+, scratch C at 4000-4007 for write tests. DL260 X inputs would be at discrete-input addresses 0..511 but pymodbus's shared-blocks mode + same-table-as-coils means those would conflict with HR seeds; FC02 tests against this profile use a separate discrete-input block instead — that's why `di size` is large but the X-input markers live in `bits` only when `shared blocks=false`. Document trade-off in README.", - "bits": [ {"_quirk": "Y0 marker. DL260 maps Y0 to coil 2048 (0-based). Coil 2048 = ON proves the mapping.", "addr": 2048, "value": 1}, diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json index 2738d6f..5d9b63a 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json @@ -1,5 +1,5 @@ { - "_comment": "Standard.json — generic Modbus TCP server for the integration suite. Loaded by `pymodbus.simulator`. See ../README.md for the launch command. Holding registers 0..31 are seeded with their address as value (HR[5]=5) for easy mental-map diagnostics. HR[100] auto-increments via pymodbus's built-in `increment` action so subscribe-and-receive integration tests have a register that ticks without a write. HR[200..209] is a scratch range left at 0 for write-roundtrip tests. Coils 0..31 alternate on/off (even=on); coils 100..109 scratch.", + "_comment": "Standard.json — generic Modbus TCP server for the integration suite. See ../README.md. NOTE: pymodbus rejects unknown keys at device-list / setup level; explanatory comments live in the README + git history. Layout: HR[0..31]=address-as-value, HR[100]=auto-increment, HR[200..209]=scratch, coils 1024..1055=alternating, coils 1100..1109=scratch. Coils live at 1024+ because pymodbus stores all 4 standard tables in ONE underlying cell array — bits and uint16 at the same address conflict (each cell can only be typed once).", "server_list": { "srv": { @@ -14,11 +14,11 @@ "device_list": { "dev": { "setup": { - "co size": 1024, - "di size": 1024, - "hr size": 1024, - "ir size": 1024, - "shared blocks": false, + "co size": 2048, + "di size": 2048, + "hr size": 2048, + "ir size": 2048, + "shared blocks": true, "type exception": false, "defaults": { "value": {"bits": 0, "uint16": 0, "uint32": 0, "float32": 0.0, "string": " "}, @@ -27,26 +27,11 @@ }, "invalid": [], "write": [ - [0, 1023] - ], - - "bits": [ - {"addr": 0, "value": 1}, {"addr": 1, "value": 0}, - {"addr": 2, "value": 1}, {"addr": 3, "value": 0}, - {"addr": 4, "value": 1}, {"addr": 5, "value": 0}, - {"addr": 6, "value": 1}, {"addr": 7, "value": 0}, - {"addr": 8, "value": 1}, {"addr": 9, "value": 0}, - {"addr": 10, "value": 1}, {"addr": 11, "value": 0}, - {"addr": 12, "value": 1}, {"addr": 13, "value": 0}, - {"addr": 14, "value": 1}, {"addr": 15, "value": 0}, - {"addr": 16, "value": 1}, {"addr": 17, "value": 0}, - {"addr": 18, "value": 1}, {"addr": 19, "value": 0}, - {"addr": 20, "value": 1}, {"addr": 21, "value": 0}, - {"addr": 22, "value": 1}, {"addr": 23, "value": 0}, - {"addr": 24, "value": 1}, {"addr": 25, "value": 0}, - {"addr": 26, "value": 1}, {"addr": 27, "value": 0}, - {"addr": 28, "value": 1}, {"addr": 29, "value": 0}, - {"addr": 30, "value": 1}, {"addr": 31, "value": 0} + [0, 31], + [100, 100], + [200, 209], + [1024, 1055], + [1100, 1109] ], "uint16": [ @@ -69,7 +54,38 @@ {"addr": 100, "value": 0, "action": "increment", - "parameters": {"minval": 0, "maxval": 65535}} + "parameters": {"minval": 0, "maxval": 65535}}, + + {"addr": 200, "value": 0}, {"addr": 201, "value": 0}, + {"addr": 202, "value": 0}, {"addr": 203, "value": 0}, + {"addr": 204, "value": 0}, {"addr": 205, "value": 0}, + {"addr": 206, "value": 0}, {"addr": 207, "value": 0}, + {"addr": 208, "value": 0}, {"addr": 209, "value": 0} + ], + + "bits": [ + {"addr": 1024, "value": 1}, {"addr": 1025, "value": 0}, + {"addr": 1026, "value": 1}, {"addr": 1027, "value": 0}, + {"addr": 1028, "value": 1}, {"addr": 1029, "value": 0}, + {"addr": 1030, "value": 1}, {"addr": 1031, "value": 0}, + {"addr": 1032, "value": 1}, {"addr": 1033, "value": 0}, + {"addr": 1034, "value": 1}, {"addr": 1035, "value": 0}, + {"addr": 1036, "value": 1}, {"addr": 1037, "value": 0}, + {"addr": 1038, "value": 1}, {"addr": 1039, "value": 0}, + {"addr": 1040, "value": 1}, {"addr": 1041, "value": 0}, + {"addr": 1042, "value": 1}, {"addr": 1043, "value": 0}, + {"addr": 1044, "value": 1}, {"addr": 1045, "value": 0}, + {"addr": 1046, "value": 1}, {"addr": 1047, "value": 0}, + {"addr": 1048, "value": 1}, {"addr": 1049, "value": 0}, + {"addr": 1050, "value": 1}, {"addr": 1051, "value": 0}, + {"addr": 1052, "value": 1}, {"addr": 1053, "value": 0}, + {"addr": 1054, "value": 1}, {"addr": 1055, "value": 0}, + + {"addr": 1100, "value": 0}, {"addr": 1101, "value": 0}, + {"addr": 1102, "value": 0}, {"addr": 1103, "value": 0}, + {"addr": 1104, "value": 0}, {"addr": 1105, "value": 0}, + {"addr": 1106, "value": 0}, {"addr": 1107, "value": 0}, + {"addr": 1108, "value": 0}, {"addr": 1109, "value": 0} ], "uint32": [],