diff --git a/_p54.json b/_p54.json new file mode 100644 index 0000000..8120253 --- /dev/null +++ b/_p54.json @@ -0,0 +1 @@ +{"title":"Phase 3 PR 54 -- Siemens S7 Modbus TCP quirks research doc","body":"## Summary\n\nAdds `docs/v2/s7.md` (485 lines) covering Siemens SIMATIC S7 family Modbus TCP behavior. Mirrors the `docs/v2/dl205.md` template for future per-quirk implementation PRs.\n\n## Key findings for the implementation track\n\n- **No fixed memory map** — every S7 Modbus server is user-wired via `MB_SERVER`/`MODBUSCP`/`MODBUSPN` library blocks. Driver must accept per-site config, not assume a vendor layout.\n- **MB_SERVER requires non-optimized DBs** (STATUS `0x8383` if optimized). Most common field bug.\n- **Word order default = ABCD** (opposite of DL260). Driver's S7 profile default must be `ByteOrder.BigEndian`, not `WordSwap`.\n- **One port per MB_SERVER instance** — multi-client requires parallel FBs on 503/504/… Most clients assume port 502 multiplexes (wrong on S7).\n- **CP 343-1 Lean is server-only**, requires the `2XV9450-1MB00` license.\n- **FC20/21/22/23/43 all return Illegal Function** on every S7 variant — driver must not attempt FC23 bulk-read optimization for S7.\n- **STOP-mode behavior non-deterministic** across firmware bands — treat both read/write STOP-mode responses as unavailable.\n\nTwo items flagged as unconfirmed rumour (V2.0+ float byte-order claim, STOP-mode caching location).\n\nNo code, no tests — implementation lands in PRs 56+.\n\n## Test plan\n- [x] Doc renders as markdown\n- [x] 31 citations present\n- [x] Section structure matches dl205.md template","head":"phase-3-pr54-s7-research-doc","base":"v2"} diff --git a/_p55.json b/_p55.json new file mode 100644 index 0000000..00c2066 --- /dev/null +++ b/_p55.json @@ -0,0 +1 @@ +{"title":"Phase 3 PR 55 -- Mitsubishi MELSEC Modbus TCP quirks research doc","body":"## Summary\n\nAdds `docs/v2/mitsubishi.md` (451 lines) covering MELSEC Q/L/iQ-R/iQ-F/FX3U Modbus TCP behavior. Mirrors `docs/v2/dl205.md` template for per-quirk implementation PRs.\n\n## Key findings for the implementation track\n\n- **Module naming trap** — `QJ71MB91` is SERIAL RTU, not TCP. TCP module is `QJ71MT91`. Surface clearly in driver docs.\n- **No canonical mapping** — per-site 'Modbus Device Assignment Parameter' block (up to 16 entries). Treat mapping as runtime config.\n- **X/Y hex vs octal depends on family** — Q/L/iQ-R use HEX (X20 = decimal 32); FX/iQ-F use OCTAL (X20 = decimal 16). Helper must take a family selector.\n- **Word order CDAB default** across all MELSEC families (opposite of Siemens S7). Driver Mitsubishi profile default: `ByteOrder.WordSwap`.\n- **D-registers binary by default** (opposite of DL205's BCD default). Caller opts in to `Bcd16`/`Bcd32` when ladder uses BCD.\n- **FX5U needs firmware ≥ 1.060** for Modbus TCP server — older is client-only.\n- **FX3U-ENET vs FX3U-ENET-P502 vs FX3U-ENET-ADP** — only the middle one binds port 502; the last has no Modbus at all. Common operator mis-purchase.\n- **QJ71MT91 does NOT support FC22 / FC23** — iQ-R / iQ-F do. Bulk-read optimization must gate on capability.\n- **STOP-mode writes configurable** on Q/L/iQ-R/iQ-F (default accept), always rejected on FX3U-ENET.\n\nThree unconfirmed rumours flagged separately.\n\nNo code, no tests — implementation lands in PRs 58+.\n\n## Test plan\n- [x] Doc renders as markdown\n- [x] 17 citations present\n- [x] Per-model test naming matrix included (`Mitsubishi_QJ71MT91_*`, `Mitsubishi_FX5U_*`, `Mitsubishi_FX3U_ENET_*`, shared `Mitsubishi_Common_*`)","head":"phase-3-pr55-mitsubishi-research-doc","base":"v2"} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/s7_1500.json b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/s7_1500.json new file mode 100644 index 0000000..d4f8a2b --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/s7_1500.json @@ -0,0 +1,77 @@ +{ + "_comment": "s7_1500.json -- Siemens SIMATIC S7-1500 + MB_SERVER quirk simulator. Models docs/v2/s7.md behaviors as concrete register values. Unlike DL260 (CDAB word order default) or Mitsubishi (CDAB default), S7 MB_SERVER uses ABCD word order by default because Siemens native CPU types are big-endian top-to-bottom both within the register pair and byte pair. This profile exists so the driver's S7 profile default ByteOrder.BigEndian can be validated end-to-end. pymodbus bit-address semantics are the same as dl205.json (FC01/02/05/15 address X maps to cell index X/16); seed bits at the appropriate cell-indexed positions.", + + "server_list": { + "srv": { + "comm": "tcp", + "host": "0.0.0.0", + "port": 5020, + "framer": "socket", + "device_id": 1 + } + }, + + "device_list": { + "dev": { + "setup": { + "co size": 4096, + "di size": 4096, + "hr size": 4096, + "ir size": 1024, + "shared blocks": true, + "type exception": false, + "defaults": { + "value": {"bits": 0, "uint16": 0, "uint32": 0, "float32": 0.0, "string": " "}, + "action": {"bits": null, "uint16": null, "uint32": null, "float32": null, "string": null} + } + }, + "invalid": [], + "write": [ + [0, 0], + [25, 25], + [100, 101], + [200, 209], + [300, 301] + ], + + "uint16": [ + {"_quirk": "DB1 header marker. On an S7-1500 with MB_SERVER pointing at DB1, operators often reserve DB1.DBW0 for a fingerprint word so clients can verify they're talking to the right DB. 0xABCD = 43981.", + "addr": 0, "value": 43981}, + + {"_quirk": "Scratch HR range 200..209 -- mirrors the standard.json scratch range so the smoke test (S7_1500Profile.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": "Float32 1.5f in ABCD word order (Siemens big-endian default, OPPOSITE of DL260 CDAB). IEEE-754 1.5 = 0x3FC00000. ABCD = high word first: HR[100]=0x3FC0=16320, HR[101]=0x0000=0.", + "addr": 100, "value": 16320}, + {"_quirk": "Float32 1.5f ABCD low word.", + "addr": 101, "value": 0}, + + {"_quirk": "Int32 0x12345678 in ABCD word order. HR[300]=0x1234=4660, HR[301]=0x5678=22136. Demonstrates the contrast with DL260 CDAB Int32 encoding.", + "addr": 300, "value": 4660}, + {"addr": 301, "value": 22136} + ], + + "bits": [ + {"_quirk": "Coil bank marker cell. S7 MB_SERVER doesn't fix coil addresses; this simulates a user-wired DB where coil 400 (=bit 0 of cell 25) represents a latched digital output. Cell 25 bit 0 = 1 proves the wire-format round-trip works for coils on S7 profile.", + "addr": 25, "value": 1}, + + {"_quirk": "Discrete-input bank marker cell. DI 500 (=bit 0 of cell 31) = 1. Like coils, discrete inputs on S7 MB_SERVER are per-site; we assert the end-to-end FC02 path only.", + "addr": 31, "value": 1} + ], + + "uint32": [], + "float32": [], + "string": [], + "repeat": [] + } + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 index 6cb9195..056f8a5 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 @@ -21,7 +21,7 @@ #> [CmdletBinding()] param( - [Parameter(Mandatory)] [ValidateSet('standard', 'dl205')] [string]$Profile, + [Parameter(Mandatory)] [ValidateSet('standard', 'dl205', 's7_1500', 'mitsubishi')] [string]$Profile, [int]$HttpPort = 8080 ) diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs new file mode 100644 index 0000000..db2004c --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs @@ -0,0 +1,44 @@ +namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7; + +/// +/// Tag map for the Siemens SIMATIC S7-1500 device class with the MB_SERVER library +/// block mapping HR[0..] to DB1.DBW0+. Mirrors s7_1500.json in Pymodbus/. +/// +/// +/// Unlike DL205, S7 has no fixed Modbus memory map — every site wires MB_SERVER to a +/// different DB. The profile here models the *default* user layout documented in +/// docs/v2/s7.md §per-model-matrix: DB1.DBW0 = fingerprint marker, a scratch HR +/// range 200..209 for write-roundtrip tests, and ABCD-order Float32 / Int32 markers at +/// HR[100..101] and HR[300..301] to prove the driver's S7 profile default is correct. +/// +public static class S7_1500Profile +{ + /// + /// Scratch HR the smoke test writes + reads. Address 200 mirrors the DL205 / + /// standard scratch range so one smoke test pattern works across all device profiles. + /// + public const ushort SmokeHoldingRegister = 200; + + /// Value the smoke test writes then reads back. + public const short SmokeHoldingValue = 4321; + + 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 }, + }; +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500SmokeTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500SmokeTests.cs new file mode 100644 index 0000000..8d89d42 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500SmokeTests.cs @@ -0,0 +1,54 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7; + +/// +/// End-to-end smoke against the S7-1500 MB_SERVER pymodbus profile (or a real +/// S7-1500 + MB_SERVER deployment when MODBUS_SIM_ENDPOINT points at one). Drives +/// the full + real stack — +/// no fake transport. Success proves the driver initializes against the S7 simulator, +/// writes a known value, and reads it back with the correct status and value, which is +/// the baseline every S7-specific test (PR 57+) builds on. +/// +/// +/// S7-specific quirk tests (MB_SERVER requires non-optimized DBs, ABCD word order +/// default, port-per-connection, FC23 Illegal Function, STOP-mode behaviour, etc.) land +/// as separate test classes in this directory as each quirk is validated in pymodbus. +/// Keep this smoke test deliberately narrow — filtering by device class +/// (--filter DisplayName~S7) should surface the quirk-specific failure mode when +/// something goes wrong, not a blanket smoke failure that could mean anything. +/// +[Collection(ModbusSimulatorCollection.Name)] +[Trait("Category", "Integration")] +[Trait("Device", "S7")] +public sealed class S7_1500SmokeTests(ModbusSimulatorFixture sim) +{ + [Fact] + public async Task S7_1500_roundtrip_write_then_read_of_holding_register() + { + if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); + if (!string.Equals(Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE"), "s7_1500", + StringComparison.OrdinalIgnoreCase)) + { + Assert.Skip("MODBUS_SIM_PROFILE != s7_1500 — skipping (other profiles don't seed the S7 scratch range identically)."); + } + + var options = S7_1500Profile.BuildOptions(sim.Host, sim.Port); + await using var driver = new ModbusDriver(options, driverInstanceId: "s7-smoke"); + await driver.InitializeAsync(driverConfigJson: "{}", TestContext.Current.CancellationToken); + + var writeResults = await driver.WriteAsync( + [new(FullReference: "Smoke_HReg200", Value: (short)S7_1500Profile.SmokeHoldingValue)], + TestContext.Current.CancellationToken); + writeResults.Count.ShouldBe(1); + writeResults[0].StatusCode.ShouldBe(0u, "write must succeed against the S7-1500 MB_SERVER profile"); + + var readResults = await driver.ReadAsync( + ["Smoke_HReg200"], + TestContext.Current.CancellationToken); + readResults.Count.ShouldBe(1); + readResults[0].StatusCode.ShouldBe(0u); + readResults[0].Value.ShouldBe((short)S7_1500Profile.SmokeHoldingValue); + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj index 0b0dad0..cbf763c 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj @@ -26,6 +26,7 @@ +