Compare commits

...

6 Commits

Author SHA1 Message Date
Joseph Doherty
f52b7d8979 Phase 3 PR 58 -- Mitsubishi MELSEC pymodbus profile + smoke integration test. Adds tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/mitsubishi.json modelling a representative MELSEC Modbus Device Assignment block: D0..D1023 -> HR[0..1023], M-relay marker at coil 512 (cell 32) and X-input marker at DI 528 (cell 33). Covers the canonical MELSEC quirks from docs/v2/mitsubishi.md: D0 fingerprint at HR[0]=0x1234 so clients can verify the assignment parameter block is in effect, scratch HR 200..209 mirroring dl205/s7_1500/standard scratch range for uniform smoke tests, Float32 1.5f at HR[100..101] in CDAB word order (HR[100]=0, HR[101]=0x3FC0) -- same as DL260, OPPOSITE of S7 ABCD, confirms MELSEC-family driver profile default must be ByteOrder.WordSwap. Int32 0x12345678 CDAB at HR[300..301]. D10 = binary 1234 (0x04D2) proves MELSEC is BINARY-by-default (opposite of DL205 BCD-by-default quirk) -- reading D10 with Bcd16 data type would throw InvalidDataException on nibble 0xD. M-relay marker cell moved to address 32 (coil 512) to avoid shared-block collision with D0 uint16 marker at cell 0; pymodbus shared-blocks=true semantics allow only one type per cell index, so Modbus-coil-0 can't coexist with Modbus-HR-0 on the same sim. Same pattern we applied to dl205 profile (X-input bank at cell 1, not cell 0, to coexist with V0 marker). Adds Mitsubishi/ test directory with MitsubishiProfile.cs (SmokeHoldingRegister=200, SmokeHoldingValue=7890, BuildOptions with probe-disabled + 2s timeout) and MitsubishiSmokeTests.cs (Mitsubishi_roundtrip_write_then_read_of_holding_register single fact that writes 7890 at HR[200] then reads back, gated on MODBUS_SIM_PROFILE=mitsubishi). csproj copies Mitsubishi/** as PreserveNewest. Per-model differences (FX5U firmware gate, QJ71MT91 FC22/23 absence, FX/iQ-F octal vs Q/L/iQ-R hex X-addressing) are handled in the MelsecAddress helper (PR 59) + per-model test classes (PR 60). Verified: smoke 1/1 passes against live mitsubishi sim. Prior S7 tests 4/4 still green when swapped back. Modbus.Tests unit suite 143/143. 2026-04-18 23:02:29 -04:00
Joseph Doherty
b54724a812 Phase 3 PR 57 -- S7 byte-order + fingerprint integration tests against s7_1500 pymodbus profile. Three facts in new S7_ByteOrderTests class: (1) S7_Float32_ABCD_decodes_1_5f_from_HR100 reads HR[100..101] with ModbusByteOrder.BigEndian AND with WordSwap on the same wire bytes; asserts BigEndian==1.5f AND WordSwap!=1.5f -- proving both that Siemens S7 stores Float32 in ABCD word order (opposite of DL260 CDAB) and that the ByteOrder flag is not a no-op on the same wire buffer. (2) S7_Int32_ABCD_decodes_0x12345678_from_HR300 reads HR[300]=0x1234 + HR[301]=0x5678 with BigEndian and asserts the reassembled Int32 = 0x12345678; documents the contrast with DL260 CDAB Int32 encoding. (3) S7_DB1_fingerprint_marker_at_HR0_reads_0xABCD reads HR[0]=0xABCD -- real MB_SERVER deployments reserve DB1.DBW0 as a fingerprint so clients can verify they're pointing at the right DB, protecting against typos in the MB_SERVER.MB_HOLD_REG.DB_number parameter. No driver code changes -- the ByteOrder.BigEndian path has existed since PR 24; this PR exists to lock in the S7-specific semantics at the integration level so future refactors of NormalizeWordOrder can't silently break S7. All 3 tests gate on MODBUS_SIM_PROFILE=s7_1500 so they skip cleanly against dl205 or standard profiles. Verified end-to-end: 4/4 S7 integration tests pass (1 smoke from PR 56 + 3 new). No regression in driver unit tests. Per the per-quirk-PR plan: the S7 quirks NOT testable via pymodbus sim (MB_SERVER STATUS 0x8383 optimized-DB behavior, port-per-connection semantics, CP 343-1 Lean license rejection, STOP-mode non-determinism) remain in docs/v2/s7.md as design guidance for driver users rather than automated tests -- they're TIA-Portal-side or CP-hardware-side behaviors that pymodbus cannot reproduce without custom Python actions. 2026-04-18 22:58:44 -04:00
Joseph Doherty
10c724b5b6 Phase 3 PR 56 -- Siemens S7-1500 pymodbus profile + smoke integration test. Adds tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/s7_1500.json modelling the SIMATIC S7-1500 + MB_SERVER default deployment documented in docs/v2/s7.md: DB1.DBW0 = 0xABCD fingerprint marker (operators reserve this so clients can verify they're talking to the right DB), scratch HR range 200..209 for write-roundtrip tests mirroring dl205.json + standard.json, Float32 1.5f at HR[100..101] in ABCD word order (high word first -- OPPOSITE of DL260 CDAB), Int32 0x12345678 at HR[300..301] in ABCD. Also seeds a coil at bit-addr 400 (= cell 25 bit 0) and a discrete input at bit-addr 500 (= cell 31 bit 0) so future S7-specific tests for FC01/FC02 have stable markers. shared blocks=true to match the proven dl205.json pattern (pymodbus's bits/uint16 cells coexist cleanly when addresses don't collide). Write list references cells (0, 25, 100-101, 200-209, 300-301), not bit addresses -- pymodbus's write-range entries are cell-indexed, not bit-indexed. Adds tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/ directory with S7_1500Profile.cs (mirrors DL205Profile pattern: SmokeHoldingRegister=200, SmokeHoldingValue=4321, BuildOptions tags + probe-disabled + 2s timeout) and S7_1500SmokeTests.cs (single fact S7_1500_roundtrip_write_then_read_of_holding_register that writes SmokeHoldingValue then reads it back, asserting both write status 0 and read status 0 + value equality). Gates on MODBUS_SIM_PROFILE=s7_1500 so the test skips cleanly against other profiles. csproj updated to copy S7/** to test output as PreserveNewest (pattern matching DL205/**). Pymodbus/serve.ps1 ValidateSet extended from {standard,dl205} to {standard,dl205,s7_1500,mitsubishi} -- mitsubishi.json lands in PR 58 but the validator slot is claimed now so the serve.ps1 diff is one line in this PR and zero lines in future PRs. Verified end-to-end: smoke test 1/1 passes against the running pymodbus s7_1500 profile (localhost:5020 FC06 write of 4321 at HR[200] + FC03 read back). 143/143 Modbus.Tests pass, no regression in driver code because this PR is purely test-asset. Per-quirk S7 integration tests (ABCD word order default, FC23 IllegalFunction, MB_SERVER STATUS 0x8383 behaviour, port-per-connection semantics) land in PR 57+. 2026-04-18 22:57:03 -04:00
8c89d603e8 Merge pull request 'Phase 3 PR 55 -- Mitsubishi MELSEC Modbus TCP quirks research doc' (#54) from phase-3-pr55-mitsubishi-research-doc into v2 2026-04-18 22:54:09 -04:00
299bd4a932 Merge pull request 'Phase 3 PR 54 -- Siemens S7 Modbus TCP quirks research doc' (#53) from phase-3-pr54-s7-research-doc into v2 2026-04-18 22:54:02 -04:00
Joseph Doherty
9e2b5b330f Phase 3 PR 54 -- Siemens S7 Modbus TCP quirks research document. 485-line doc at docs/v2/s7.md mirroring the docs/v2/dl205.md template for the Siemens SIMATIC S7 family (S7-1200 / S7-1500 / S7-300 / S7-400 / ET 200SP / CP 343-1 / CP 443-1 / CP 343-1 Lean / MODBUSPN). Siemens S7 is fundamentally different from DL260: there is no fixed Modbus memory map baked into firmware -- every deployment runs MB_SERVER (S7-1200/1500/ET 200SP), MODBUSCP (S7-300/400 + CP), or MODBUSPN (S7-300/400 PN) library blocks wired up to user DBs via the MB_HOLD_REG / ADDR parameters. The driver's job is therefore to handle per-site CONFIG rather than per-family QUIRKS, and the doc makes that explicit. Key findings worth flagging for the PR 56+ implementation track: (1) S7 has no fixed memory map -- must accept per-site DriverConfig, cannot assume vendor-standard layout. (2) MB_SERVER requires NON-optimized DBs in TIA Portal; optimized DBs cause the library to return STATUS 0x8383 on every access -- the single most common S7 Modbus deployment bug in the field. (3) Word order is ABCD by default (big-endian bytes + big-endian words) across all Siemens S7 Modbus paths, which is the OPPOSITE of DL260 CDAB -- the Modbus driver's S7 profile default must be ByteOrder.BigEndian, not WordSwap. (4) MB_SERVER listens on ONE port per FB instance; multi-client support requires running MB_SERVER on 502 / 503 / 504 / ... simultaneously -- most clients assume port 502 multiplexes, which is wrong on S7. (5) CP 343-1 Lean is SERVER-ONLY and requires the separate 2XV9450-1MB00 MODBUS TCP CP library license; client mode calls return immediate error on Lean. (6) MB_SERVER does NOT filter Unit ID, accepts any value. Means the driver can't use Unit ID to detect 'direct vs gateway' topology. (7) FC23 Read-Write Multiple, FC22 Mask Write, FC20/21 File Records, FC43 Device Identification all return exception 01 Illegal Function on every S7 variant -- the driver MUST NOT attempt bulk-read optimisation via FC23 when talking to S7. (8) STOP-mode read/write behaviour is non-deterministic across firmware bands: reads may return cached data (library internal buffer), writes may succeed-silently or return exception 04 depending on CPU firmware version -- flagged as 'driver treats both as unavailable, do not distinguish'. Unconfirmed rumours flagged separately: 'V2.0+ reverses float byte order' claim (cited but not reproduced), STOP-mode caching location (folklore, no primary source). Per-model test differentiation section names the tests as S7_<model>_<behavior> matching the DL205 template convention (e.g. S7_1200_MB_SERVER_requires_non_optimized_DB, S7_343_1_Lean_rejects_client_mode, S7_FC23_returns_IllegalFunction). 31 cited references across the Siemens Industry Online Support entry-ID system (68011496 for MB_SERVER FAQ, etc.), TIA Portal library manuals, and three third-party driver vendor release notes (Kepware, Ignition, FactoryTalk). This is a pure documentation PR -- no code, no tests, no csproj changes. Per-quirk implementation lands in PRs 56+. Research conducted 2026-04-18 against latest publicly-available Siemens documentation; STOP-mode behaviour and MB_SERVER versioning specifically cross-checked against Siemens forum answers from 2024-2025. 2026-04-18 22:50:51 -04:00
12 changed files with 968 additions and 1 deletions

1
_p54.json Normal file
View File

@@ -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"}

1
_p55.json Normal file
View File

@@ -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"}

485
docs/v2/s7.md Normal file
View File

@@ -0,0 +1,485 @@
# Siemens SIMATIC S7 (S7-1200 / S7-1500 / S7-300 / S7-400 / ET 200SP) — Modbus TCP quirks
Siemens S7 PLCs do *not* speak Modbus TCP natively at the OS/firmware level. Every
S7 Modbus-TCP-server deployment is either (a) the **`MB_SERVER`** library block
running on the CPU's PROFINET port (S7-1200 / S7-1500 / CPU 1510SP-series
ET 200SP), or (b) the **`MODBUSCP`** function block running on a separate
communication processor (**CP 343-1 / CP 343-1 Lean** on S7-300, **CP 443-1** on
S7-400), or (c) the **`MODBUSPN`** block on an S7-1500 PN port via a licensed
library. That means the quirks a Modbus client has to cope with are as much
"this is how the user's PLC programmer wired the library block up" as "this is
how the firmware behaves" — the byte-order and coil-mapping rules aren't
hard-wired into silicon like they are on a DL260. This document catalogues the
behaviours a driver has to handle across the supported model/CP variants, cites
primary sources, and names the ModbusPal integration test we'd write for each
(convention from `docs/v2/modbus-test-plan.md`: `S7_<model>_<behavior>`).
## Model / CP Capability Matrix
| PLC family | Modbus TCP server mechanism | Modbus TCP client mechanism | License required? | Typical port 502 source |
|---------------------|------------------------------------|------------------------------------|-----------------------|-----------------------------------------------------------|
| S7-1200 (V4.0+) | `MB_SERVER` on integrated PN port | `MB_CLIENT` | No (in TIA Portal) | CPU's onboard Ethernet [1][2] |
| S7-1500 (all) | `MB_SERVER` on integrated PN port | `MB_CLIENT` | No (in TIA Portal) | CPU's onboard Ethernet [1][3] |
| S7-1500 + CP 1543-1 | `MB_SERVER` on CP's IP | `MB_CLIENT` | No | Separate CP IP address [1] |
| ET 200SP CPU (1510SP, 1512SP) | `MB_SERVER` on PN port | `MB_CLIENT` | No | CPU's onboard Ethernet [3] |
| S7-300 + CP 343-1 / CP 343-1 Lean | `MODBUSCP` (FB `MODBUSCP`, instance DB per connection) | Same FB, client mode | **Yes — 2XV9450-1MB00** per CP | CP's Ethernet port [4][5] |
| S7-400 + CP 443-1 | `MODBUSCP` | `MODBUSCP` client mode | **Yes — 2XV9450-1MB00** per CP | CP's Ethernet port [4] |
| S7-400H + CP 443-1 (redundant H) | `MODBUSCP_REDUNDANT` / paired FBs | Not typical | Yes | Paired CPs in H-system [6] |
| S7-300 / S7-400 CPU PN (e.g. CPU 315-2 PN/DP) | `MODBUSPN` library | `MODBUSPN` client mode | **Yes** — Modbus-TCP PN CPU lib | CPU's PN port [7] |
| "CP 343-1 Lean" | **Server only** (no client mode supported by Lean) | — | Yes, but with restrictions | CP's Ethernet port [4][5] |
- **CP 343-1 Lean is server-only.** It can host `MODBUSCP` in server mode only;
client calls return an immediate error. A surprising number of "Lean + client
doesn't work" forum posts trace back to this [5].
- **Pure OPC UA / PROFINET CPs (CP 1542SP-1, CP 1543-1)** support Modbus TCP on
S7-1500 via the same `MB_SERVER`/`MB_CLIENT` instructions by passing the
CP's `hw_identifier`. There is no separate "Modbus CP" license needed on
S7-1500, unlike S7-300/400 [1].
- **No S7 Modbus server supports function codes 20/21 (file records),
22 (mask write), 23 (read-write multiple), or 43 (device identification).**
Sending any of these returns exception `01` (Illegal Function) on every S7
variant [1][4]. Our driver must not negotiate FC23 as a "bulk-read optimization"
when the profile is S7.
Test names:
`S7_1200_MBSERVER_Loads_OB1_Cyclic`,
`S7_CP343_Lean_Client_Mode_Rejected`,
`S7_All_FC23_Returns_IllegalFunction`.
## Address / DB Mapping
S7 Modbus servers **do not auto-expose PLC memory** — the PLC programmer has to
wire one area per Modbus table to a DB or process-image region. This is the
single biggest difference vs. DL205/Modicon/etc., where the memory map is
fixed at the factory. Our driver must therefore be tolerant of "the same
`40001` means completely different things on two S7-1200s on the same site."
### S7-1200 / S7-1500 `MB_SERVER`
The `MB_SERVER` instance exposes four Modbus tables to each connected client;
each table's backing storage is a per-block parameter [1][8]:
| Modbus table | FCs | Backing parameter | Default / typical backing |
|---------------------|-------------|-----------------------------|-----------------------------|
| Coils (0x) | FC01, FC05, FC15 | *implicit* — Q process image | `%Q0.0``%Q1023.7` (→ coil addresses 08191) [1][9] |
| Discrete Inputs (1x)| FC02 | *implicit* — I process image | `%I0.0``%I1023.7` (→ discrete addresses 08191) [1][9] |
| Input Registers (3x)| FC04 | *implicit* — M memory or DB (version-dependent) | Some firmware routes FC04 through the same MB_HOLD_REG buffer [1][8] |
| Holding Registers (4x)| FC03, FC06, FC16 | `MB_HOLD_REG` pointer | User DB (e.g. `DB10.DBW0`) or `%MW` area [1][2][8] |
- **`MB_HOLD_REG` is a pointer (VARIANT / ANY) into a user-defined DB** whose
first byte is holding-register 0 (`40001` in 1-based Modicon form). Byte
offset 2 is register 1, byte offset 4 is register 2, etc. [1][2].
- **The DB *must* have "Optimized block access" UNCHECKED.** Optimized DBs let
the compiler reorder fields for alignment; Modbus requires fixed byte
offsets. With optimized access on, the compiler accepts the project but
`MB_SERVER` returns STATUS `0x8383` (misaligned access) or silently reads
zeros [8][10][11]. This is the #1 support-forum complaint.
- **FC01/FC02/FC05/FC15 hit the Q and I process images directly — not the
`MB_HOLD_REG` DB.** Coil address 0 = `%Q0.0`, coil 1 = `%Q0.1`, coil 8 =
`%Q1.0`. The S7-1200 system manual publishes this mapping as `00001 → Q0.0`
through `09999 → Q1023.7` and `10001 → I0.0` through `19999 → I1023.7` in
1-based form; on the wire (0-based) that's coils 0-8191 and discrete inputs
0-8191 [9].
- **`%M` markers are NOT automatically exposed.** To expose `%M` over Modbus
the programmer must either (a) copy `%M` to the `MB_HOLD_REG` DB each scan,
or (b) define an Array\[0..n\] of Bool inside that DB and copy bits in/out
of `%M`. Siemens has no "MB_COIL_REG" parameter analogous to
`MB_HOLD_REG` — this confuses users migrating from Schneider [9][12].
- **Bit ordering within a Modbus holding register sourced from an `Array of
Bool`**: S7 stores bool\[0\] at `DBX0.0` which is bit 0 of byte 0 which is
the **low byte, low bit** of Modbus register `40001`. A naive client that
reads register `40001` and masks `0x0001` gets bool\[0\]. A client that
masks `0x8000` gets bool\[15\] because the high byte of the Modbus register
is the *second* byte of the DB. Siemens programmers routinely get this
wrong in the DB-via-DBX form; `Array[0..n] of Bool` is the recommended
layout because it aligns naturally [12][13].
### S7-300/400 + CP 343-1 / CP 443-1 `MODBUSCP`
Different paradigm: per-connection **parameter DB** (template
`MODBUS_PARAM_CP`) declares a table of up to 8 register-area mappings. Each
mapping is a tuple `(data_type, DB#, start_offset, length)` where `data_type`
picks the Modbus table [4]:
- `B#16#1` = Coils
- `B#16#2` = Discrete Inputs
- `B#16#3` = Holding Registers
- `B#16#4` = Input Registers
The `holding_register_start` and analogous `coils_start` parameters declare
**which Modbus address range** the CP will serve, and the DB pointers say
where in S7 memory that range lives [4][14]. Unlike `MB_SERVER`, the CP does
not reach into `%Q`/`%I` directly — *everything* goes through a DB. If an
address outside the declared ranges is requested, the CP returns exception
`02` (Illegal Data Address) [4].
Test names:
`S7_1200_FC03_Reg0_Reads_DB10_DBW0`,
`S7_1200_Optimized_DB_Returns_0x8383_MisalignedAccess`,
`S7_1200_FC01_Coil0_Reads_Q0_0`,
`S7_CP343_FC03_Outside_ParamBlock_Range_Returns_IllegalDataAddress`.
## Data Types and Byte Order
Siemens CPUs store scalars **big-endian** internally ("Motorola format"), which
is the same byte order Modbus specifies inside each register. So for 16-bit
values (`Int`, `Word`, `UInt`) the on-the-wire layout is straightforward
`AB` — high byte of the PLC value in the high byte of the Modbus register
[15][16]. No byte-swap trap for 16-bit types.
The trap is 32-bit types (`DInt`, `DWord`, `Real`). Here's what actually
happens across the S7 family:
### S7-1200 / S7-1500 `MB_SERVER`
- **The backing DB stores 32-bit values in big-endian byte order, high word
first** — i.e. `ABCD` when viewed as two consecutive Modbus registers. A
`Real` at `DB10.DBD0` with value `0x12345678` reads over Modbus as
register 0 = `0x1234`, register 1 = `0x5678` [15][16][17].
- **This is `ABCD`, *not* `CDAB`.** Clients that hard-code CDAB (common default
for meters and VFDs) will get wildly wrong floats. Configure the S7 profile
with `WordOrder = ABCD` (aka "big-endian word + big-endian byte" aka
"high-word first") [15][17].
- **`MB_SERVER` does not swap.** It's a direct memcpy from the DB bytes to
the Modbus payload. Whatever byte order the ladder programmer stored into
the DB is what the client receives [17]. This means a programmer who used
`MOVE_BLK` from two separate `Word`s into `DBD` with the "wrong" order can
produce `CDAB` without realising.
- **`Real` is IEEE 754 single-precision** — unambiguous, no BCD trap like on
DL series [15].
- **Strings**: S7 `String[n]` has a 2-byte header (max length, current length)
*before* the character bytes. A client reading a string over Modbus gets
the header in the first register and then the characters two-per-register
in high-byte-first order. `WString` is UTF-16 and the header is 4 bytes
[18]. Our driver's string decoder must expose the "skip header" option for
S7 profile.
### S7-300/400 `MODBUSCP` (CP 343-1 / CP 443-1)
- The CP writes the exact DB bytes onto the wire — again `ABCD` if the DB
stores `DInt`/`Real` in native Siemens order [4].
- **`MODBUSCP` has no `data_type` byte-swap knob.** (The `data_type` parameter
names the Modbus table, not the byte order — see the Address Mapping
section.) If the other end of the link expects `CDAB`, the programmer has
to swap words in ladder before writing the DB [4][14].
### Operator-reported oddity
- Some S7 drivers (Kepware's "Siemens TCP/IP Ethernet" driver, Ignition's
"Siemens S7" driver) expose a per-tag `Float Byte Order` with options
`ABCD`/`CDAB`/`BADC`/`DCBA` because end-users have encountered every
permutation in the field — not because the PLC natively swaps, but because
ladder programmers have historically stored floats every which way [19].
Our S7 Modbus profile should default to `ABCD` but expose a per-tag
override.
- **Unconfirmed rumour**: that S7-1500 firmware V2.0+ reverses float byte
order for `MB_CLIENT` only. Not reproduced; the Siemens forum thread that
launched it was a user error (the remote server was the swapper, not the
S7) [20]. Treat as false until proven.
Test names:
`S7_1200_Real_WordOrder_ABCD_Default`,
`S7_1200_DInt_HighWord_First_At_DBD0`,
`S7_1200_String_Header_First_Two_Bytes`,
`S7_CP343_No_Internal_ByteSwap`.
## Coil / Discrete Input Mapping
On `MB_SERVER` the mapping from coil address → S7 bit is fixed at the
process-image level [1][9][12]:
| Modbus coil / discrete input addr | S7 address | Notes |
|-----------------------------------|---------------|-------------------------------------|
| Coil 0 (FC01/05/15) | `%Q0.0` | bit 0 of output byte 0 |
| Coil 7 | `%Q0.7` | bit 7 of output byte 0 |
| Coil 8 | `%Q1.0` | bit 0 of output byte 1 |
| Coil 8191 (max) | `%Q1023.7` | highest exposed output bit |
| Discrete input 0 (FC02) | `%I0.0` | bit 0 of input byte 0 |
| Discrete input 8191 | `%I1023.7` | highest exposed input bit |
Formulas:
```
coil_addr = byte_index * 8 + bit_index (e.g. %Q5.3 → coil 43)
discr_addr = byte_index * 8 + bit_index (e.g. %I10.2 → disc 82)
```
- **1-based Modicon form adds 1:** coil 0 (wire) = `00001` (Modicon), etc.
Our driver sends the 0-based PDU form, so `%Q0.0` writes to wire address 0.
- **Writing FC05/FC15 to `%Q` is accepted even while the CPU is in STOP** —
the PLC's process image doesn't care about the user program state. But the
output won't propagate to the physical module until RUN (see STOP section
below) [1][21].
- **`%M` markers require a DB-backed `Array of Bool`** as described in the
Address Mapping section. Our driver can't assume "coil N = MN.0" like it
can on Modicon — on S7 it's always Q/I unless the programmer built a
mapping DB [12].
- **Bit-inside-holding-register**: for `Array of Bool` inside the
`MB_HOLD_REG` DB, bool[0] is bit 0 of byte 0 → **low byte, low bit** of
Modbus register 40001. Most third-party clients probe this in the low
byte, so the common case works; the less-common case (bool[8]) is bit 0 of
byte 1 → **high byte, low bit** of Modbus register 40001. Clients that
test only bool[0] will pass and miss the mis-alignment on bool[8] [12][13].
Test names:
`S7_1200_Coil_0_Is_Q0_0`,
`S7_1200_Coil_8_Is_Q1_0`,
`S7_1200_Discrete_Input_7_Is_I0_7`,
`S7_1200_Coil_Write_In_STOP_Accepted_But_Output_Frozen`.
## Function Code Support & Max Registers Per Request
| FC | Name | S7-1200 / S7-1500 MB_SERVER | CP 343-1 / CP 443-1 MODBUSCP | Max qty per request |
|----|----------------------------|-----------------------------|------------------------------|--------------------------------|
| 01 | Read Coils | Yes | Yes | 2000 bits (spec) |
| 02 | Read Discrete Inputs | Yes | Yes | 2000 bits (spec) |
| 03 | Read Holding Registers | Yes | Yes | **125** (spec max) |
| 04 | Read Input Registers | Yes | Yes | **125** |
| 05 | Write Single Coil | Yes | Yes | 1 |
| 06 | Write Single Register | Yes | Yes | 1 |
| 15 | Write Multiple Coils | Yes | Yes | 1968 bits (spec) — *see note* |
| 16 | Write Multiple Registers | Yes | Yes | **123** (spec max for TCP) |
| 07 | Read Exception Status | No (RTU only) | No | — |
| 17 | Report Server ID | No | No | — |
| 20/21 | Read/Write File Record | No | No | — |
| 22 | Mask Write Register | No | No | — |
| 23 | Read/Write Multiple | No | No | — |
| 43 | Read Device Identification | No | No | — |
- **S7-1200/1500 honour the full spec maxima** for FC03/04 (125) and FC16
(123) [1][22]. No sub-spec cap like DL260's 100-register FC16 limit.
- **FC15 (Write Multiple Coils) on `MB_SERVER`** writes into `%Q`, which maxes
out at 1024 bytes = 8192 bits, but the spec's 1968-bit per-request limit
caps any single call first [1][9].
- **`MB_HOLD_REG` buffer size is bounded by DB size** — max DB size on
S7-1200 is 64 KB, on S7-1500 is much larger (several MB depending on CPU),
so the practical `MB_HOLD_REG` limit is 32767 16-bit registers on S7-1200
and effectively unbounded on S7-1500 [22][23]. The *per-request* limit is
still 125.
- **Read past the end of `MB_HOLD_REG`** returns exception `02` (Illegal
Data Address) at the start of the overflow register, not a partial read
[1][8].
- **Request larger than spec max** (e.g. FC03 quantity 126) returns exception
`03` (Illegal Data Value). Verified on S7-1200 V4.2 [1][24].
- **CP 343-1 `MODBUSCP` per-request maxima are spec** (125/125/123/1968/2000),
matching the standard [4]. The CP's `MODBUS_PARAM_CP` caps the total
*exposed* range, not the per-call quantity.
Test names:
`S7_1200_FC03_126_Registers_Returns_IllegalDataValue`,
`S7_1200_FC16_124_Registers_Returns_IllegalDataValue`,
`S7_1200_FC03_Past_MB_HOLD_REG_End_Returns_IllegalDataAddress`,
`S7_1200_FC17_ReportServerId_Returns_IllegalFunction`.
## Exception Codes
S7 Modbus servers return only the four standard exception codes [1][4]:
| Code | Name | Triggered by |
|------|-----------------------|----------------------------------------------------------------------|
| 01 | Illegal Function | FC not in the supported list (17, 20-23, 43, any undefined FC) |
| 02 | Illegal Data Address | Register outside `MB_HOLD_REG` / outside `MODBUSCP` param-block range |
| 03 | Illegal Data Value | Quantity exceeds spec (FC03/04 > 125, FC16 > 123, FC01/02 > 2000, FC15 > 1968) |
| 04 | Server Failure | Runtime error inside MB_SERVER (DB access fault, corrupt DB header, MB_SERVER disabled mid-request) [1][24] |
- **No proprietary exception codes (05/06/0A/0B) are used** on any S7
Modbus server [1][4]. Our driver's status-code mapper can treat these as
"never observed" on the S7 profile.
- **CPU in STOP → `MB_SERVER` keeps running if it's in OB1 of the firmware's
communication task, but OB1 itself is not scanned.** In practice:
- Holding-register *reads* (FC03) continue to return the last DB values
frozen at the moment the CPU entered STOP. The `MB_SERVER` block is in
OB1 so it isn't re-invoked; however the TCP stack keeps the socket open
and returns cached data on subsequent polls [1][21]. **Unconfirmed**
whether this is cached in the CP or in the CPU's communication processor;
behaviour varies between firmware 4.0 and 4.5 [21].
- Holding-register *writes* (FC06/FC16) during STOP return exception `04`
(Server Failure) on S7-1200 V4.2+, and return success-but-discarded on
older firmware [1][24]. Our driver should treat FC06/FC16 during STOP as
non-deterministic and not rely on the response code.
- Coil *writes* (FC05/FC15) to `%Q` are *accepted* by the process image
during STOP, but the physical output freezes at its last RUN-mode value
(or the configured STOP-mode substitute value) until RUN resumes [1][21].
- **Writing a read-only address via FC06/FC16**: returns `02` (Illegal Data
Address), not `04`. S7 does not have "write-protected" holding registers —
the programmer either exposes a DB for read-write or doesn't expose it at
all [1][12].
STATUS codes (returned in the `STATUS` output of the block, not on the wire):
- `0x0000` — no error.
- `0x7001` — first call, connection being established.
- `0x7002` — subsequent cyclic call, connection in progress.
- `0x8383` — data access error (optimized DB, DB too small, or type mismatch)
[10][24].
- `0x8188` — invalid parameter combination (e.g. MB_MODE out of range) [24].
- `0x80C8` — mismatched UNIT_ID between MB_CLIENT and `MB_SERVER` [25].
Test names:
`S7_1200_FC03_Outside_HoldReg_Returns_IllegalDataAddress`,
`S7_1200_FC16_In_STOP_Returns_ServerFailure`,
`S7_1200_FC03_In_STOP_Returns_Cached_Values`,
`S7_1200_No_Proprietary_ExceptionCodes_0x05_0x06_0x0A_0x0B`.
## Connection Behavior
- **Max simultaneous Modbus TCP connections**:
- **S7-1200**: shares a pool of 8 open-communication connections across
all TCP/UDP/Modbus use. On a CPU 1211C you get 8 total; on 1215C/1217C
still 8 shared among PG/HMI/OUC/Modbus. Each `MB_SERVER` instance
reserves one. A typical site with a PG + 1 HMI + 2 Modbus clients uses
4 of the 8 [1][26].
- **S7-1500**: up to **8 concurrent Modbus TCP server connections** per
`MB_SERVER` port, across multiple `MB_SERVER` instance DBs each with a
unique port. Total open-communication resources depend on CPU (e.g.
CPU 1515-2 PN supports 128 OUC connections total; Modbus is a subset)
[1][27].
- **CP 343-1 Lean**: up to **8** simultaneous Modbus TCP connections on
port 502 [4][5]. Exceeding this refuses at TCP accept.
- **CP 443-1 Advanced**: up to **16** simultaneous Modbus TCP connections
[4].
- **Multi-connection model on `MB_SERVER`**: one instance DB per connection.
An instance DB listening on port 502 serves exactly one connection at a
time; to serve N simultaneous clients you need N instance DBs each with a
unique port (502/503/504...). **This is a real trap** — most users expect
port 502 to multiplex [27][28]. Our driver must not assume port 502 is the
only listener.
- **Keep-alive**: S7-1500's TCP stack does send TCP keepalives (default
every ~30 s) but the interval is not exposed as a configurable. S7-1200 is
the same. CP 343-1 keepalives are configured via HW Config → CP properties
→ Options → "Send keepalive" (default **off** on older firmware, default
**on** on firmware V3.0+) [1][29]. Driver-side keepalive is still
advisable for S7-300/CP 343-1 on old firmware.
- **Idle-timeout close**: `MB_SERVER` does *not* close idle sockets on its
own. However, the TCP stack on S7-1500 will close a socket that fails
three consecutive keepalive probes (~2 minutes). Forum reports describe
`MB_SERVER` connections "dying overnight" on S7-1500 when an HMI stops
polling — the fix is to enable driver-side periodic reads or driver-side
TCP keepalive [29][30].
- **Reconnect after power cycle**: MB_SERVER starts listening ~1-2 seconds
after the CPU reaches RUN. If the client reconnects during STARTUP OB
(OB100), the connection is refused until OB1 runs the block at least once.
Our driver should back off and retry on `ECONNREFUSED` for the first 5
seconds after a power-cycle detection [1][24].
- **Unit Identifier**: `MB_SERVER` accepts **any** Unit ID by default — there
is no configurable filter; the PLC ignores the Unit ID field entirely.
`MB_CLIENT` defaults to Unit ID = 255 as "ignore" [25][31]. Some
third-party Modbus-TCP gateways *require* a specific Unit ID; sending
anything to S7 is safe. **CP 343-1 `MODBUSCP`** also accepts any Unit ID
in server mode, but the parameter DB exposes a `single_write` / `unit_id`
field on newer firmware to allow filtering [4].
Test names:
`S7_1200_9th_TCP_Connection_Refused_On_8_Conn_Pool`,
`S7_1500_Port_503_Required_For_Second_Instance`,
`S7_1200_Reconnect_After_Power_Cycle_Succeeds_Within_5s`,
`S7_1200_Unit_ID_Ignored_Any_Accepted`.
## Behavioral Oddities
- **Transaction ID echo** is reliable on all S7 variants. `MB_SERVER` copies
the MBAP TxId verbatim. No known firmware that drops TxId under load [1][31].
- **Request serialization**: a single `MB_SERVER` instance serializes
requests from its one connected client — the block processes one PDU per
call and calls happen once per OB1 scan. OB1 scan time of 5-50 ms puts an
upper bound on throughput at ~20-200 requests/sec per connection [1][30].
Multiple `MB_SERVER` instances (one per port) run in parallel because OB1
calls them sequentially within the same scan.
- **OB1 scan coupling**: `MB_SERVER` must be called cyclically from OB1 (or
another cyclic OB). If the programmer puts it in a conditional branch
that doesn't fire every scan, requests time out. The STATUS `0x7002`
"in progress" is *expected* between calls, not an error [1][24].
- **Optimized DB backing `MB_HOLD_REG`** — already covered in Address
Mapping; STATUS becomes `0x8383`. This is the most common deployment bug
on S7-1500 projects migrated from older S7-1200 examples [10][11].
- **CPU STOP behaviour** — covered in Exception Codes section. The short
version: reads may return stale data without error; writes return exception
04 on modern firmware.
- **Partial-frame disconnect**: S7-1200/1500 TCP stack closes the socket on
any MBAP header where the `Length` field doesn't match the PDU length.
Driver must detect half-close and reconnect [1][29].
- **MBAP `Protocol ID` must be 0**. Any non-zero value causes the CP/CPU to
drop the frame silently (no response, no RST) on S7-1500 firmware V2.0
through V2.9; firmware V3.0+ sends an RST [1][30]. *Unconfirmed* whether
V3.1 still sends RST or returns to silent drop.
- **FC01/FC02 access outside `%Q`/`%I` range**: on S7-1200, requesting
coil address 8192 (= `%Q1024.0`) returns exception `02` (Illegal Data
Address) [1][9]. The 8192-bit hard cap is a process-image size limit on
the CPU, not a Modbus protocol limit.
- **`MB_CLIENT` UNIT_ID mismatch with remote `MB_SERVER`** produces STATUS
`0x80C8` on the client side, and the server silently discards the frame
(no response on the wire) [25]. This matters for Modbus-TCP-to-RTU
gateway scenarios where the Unit ID picks the RTU slave.
- **Non-IEEE REAL / BCD**: S7 does *not* use BCD like DirectLOGIC. `Real` is
always IEEE 754 single-precision. `LReal` (8-byte double) occupies 4
Modbus registers in `ABCDEFGH` order (big-endian byte, big-endian word)
[15][18].
- **`MODBUSCP` single-write** on CP 343-1: a parameter `single_write` in the
param DB controls whether FC06 on a register in the "holding register"
area triggers a callback to the user program vs. updates the DB directly.
Default is direct update. If a ladder programmer enables the callback
without implementing the callback OB, FC06 writes hang for 5 seconds then
return exception `04` [4].
Test names:
`S7_1200_TxId_Preserved_Across_Burst_Of_50_Requests`,
`S7_1200_MBSERVER_Throughput_Capped_By_OB1_Scan`,
`S7_1200_MBAP_ProtocolID_NonZero_Frame_Dropped`,
`S7_1200_Partial_MBAP_Causes_Half_Close`.
## Model-specific Differences Worth Separate Test Coverage
- **S7-1200 V4.0 vs V4.4+**: Older firmware does not support `WString` over
`MB_HOLD_REG` and returns `0x8383` if the DB contains one [18][24]. Test
both firmware bands separately.
- **S7-1500 vs S7-1200**: S7-1500 supports multiple `MB_SERVER` instances on
the *same* CPU with different ports cleanly; S7-1200 can too but its
8-connection pool is shared tighter [1][27]. Throughput per-connection is
~5× faster on S7-1500 because the comms task runs on a dedicated core.
- **S7-300 + CP 343-1 vs S7-1200/1500**: parameter-block mapping (not
`MB_HOLD_REG` pointer), per-connection license, no `%Q`/`%I` direct
access for coils (everything goes through a DB), different STATUS codes
(`DONE`/`ERROR`/`STATUS` word pairs vs. the single STATUS word) [4][14].
Driver-side it's a different profile.
- **CP 343-1 Lean vs CP 343-1 Advanced**: Lean is server-only; Advanced is
client + server. Lean's max connections = 8; Advanced = 16 [4][5].
- **CP 443-1 in S7-400H**: uses `MODBUSCP_REDUNDANT` which presents two
Ethernet endpoints that fail over. Our driver's redundancy support should
recognize the S7-400H profile as "two IP addresses, same server state,
advertise via `ServerUriArray`" [6].
- **ET 200SP CPU (1510SP / 1512SP)**: behaves as S7-1500 from `MB_SERVER`
perspective. No known deltas [3].
## References
1. Siemens Industry Online Support, *Modbus/TCP Communication between SIMATIC S7-1500 / S7-1200 and Modbus/TCP Controllers with Instructions `MB_CLIENT` and `MB_SERVER`*, Entry ID 102020340, V6 (Feb 2021). https://cache.industry.siemens.com/dl/files/340/102020340/att_118119/v6/net_modbus_tcp_s7-1500_s7-1200_en.pdf
2. Siemens TIA Portal Online Docs, *MB_SERVER instruction*. https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_eses_20/communication-processor-and-modbus-tcp/modbus-communication/modbus-tcp/modbus-tcp-instructions/mb_server-communicate-using-profinet-as-modbus-tcp-server-instruction
3. Siemens, *SIMATIC S7-1500 Communication Function Manual* (covers ET 200SP CPU). http://public.eandm.com/Public_Docs/s71500_communication_function_manual_en-US_en-US.pdf
4. Siemens Industry Online Support, *SIMATIC Modbus/TCP communication using CP 343-1 and CP 443-1 — Programming Manual*, Entry ID 103447617. https://cache.industry.siemens.com/dl/files/617/103447617/att_106971/v1/simatic_modbus_tcp_cp_en-US_en-US.pdf
5. Siemens Industry Online Support FAQ *"Which technical data applies for the SIMATIC Modbus/TCP software for CP 343-1 / CP 443-1?"*, Entry ID 104946406. https://www.industry-mobile-support.siemens-info.com/en/article/detail/104946406
6. Siemens Industry Online Support, *Redundant Modbus/TCP communication via CP 443-1 in S7-400H systems*, Entry ID 109739212. https://cache.industry.siemens.com/dl/files/212/109739212/att_887886/v1/SIMATIC_modbus_tcp_cp_red_e_en-US.pdf
7. Siemens Industry Online Support, *SIMATIC MODBUS (TCP) PN CPU Library — Programming and Operating Manual 06/2014*, Entry ID 75330636. https://support.industry.siemens.com/cs/attachments/75330636/ModbusTCPPNCPUen.pdf
8. DMC Inc., *Using an S7-1200 PLC as a Modbus TCP Slave*. https://www.dmcinfo.com/blog/27313/using-an-s7-1200-plc-as-a-modbus-tcp-slave/
9. Siemens, *SIMATIC S7-1200 System Manual* (V4.x), "MB_SERVER" pages 736-742. https://www.manualslib.com/manual/1453610/Siemens-S7-1200.html?page=736
10. lamaPLC, *Simatic Modbus S7 error- and statuscodes*. https://www.lamaplc.com/doku.php?id=simatic:errorcodes
11. ScadaProtocols, *How to Configure Modbus TCP on Siemens S7-1200 (TIA Portal Step-by-Step)*. https://scadaprotocols.com/modbus-tcp-siemens-s7-1200-tia-portal/
12. Industrial Monitor Direct, *Reading and Writing Memory Bits via Modbus TCP on S7-1200*. https://industrialmonitordirect.com/blogs/knowledgebase/reading-and-writing-memory-bits-via-modbus-tcp-on-s7-1200
13. PLCtalk forum *"Siemens S7-1200 modbus understanding"*. https://www.plctalk.net/forums/threads/siemens-s7-1200-modbus-understanding.104119/
14. Siemens SIMATIC S7 Manual, "Function block MODBUSCP — Functionality" (ManualsLib p29). https://www.manualslib.com/manual/1580661/Siemens-Simatic-S7.html?page=29
15. Chipkin, *How Real (Floating Point) and 32-bit Data is Encoded in Modbus*. https://store.chipkin.com/articles/how-real-floating-point-and-32-bit-data-is-encoded-in-modbus-rtu-messages
16. Siemens Industry Online Support forum, *MODBUS DATA conversion in S7-1200 CPU*, Entry ID 97287. https://support.industry.siemens.com/forum/WW/en/posts/modbus-data-converson-in-s7-1200-cpu/97287
17. Industrial Monitor Direct, *Siemens S7-1500 MB_SERVER Modbus TCP Configuration Guide*. https://industrialmonitordirect.com/de/blogs/knowledgebase/siemens-s7-1500-mb-server-modbus-tcp-configuration-guide
18. Siemens TIA Portal, *Data types in SIMATIC S7-1200/1500 — String/WString header layout* (system manual, "Elementary Data Types").
19. Kepware / PTC, *Siemens TCP/IP Ethernet Driver Help*, "Byte / Word Order" tag property. https://www.opcturkey.com/uploads/siemens-tcp-ip-ethernet-manual.pdf
20. Siemens SiePortal forum, *Transfer float out of words*, Entry ID 187811. https://sieportal.siemens.com/en-ww/support/forum/posts/transfer-float-out-of-words/187811 _(operator-reported "S7 swaps float" claim — traced to remote-device issue; **unconfirmed**.)_
21. Siemens SiePortal forum, *S7-1200 communication with Modbus TCP*, Entry ID 133086. https://support.industry.siemens.com/forum/WW/en/posts/s7-1200-communication-with-modbus-tcp/133086
22. Siemens SiePortal forum, *S7-1500 MB Server Holding Register Max Word*, Entry ID 224636. https://support.industry.siemens.com/forum/WW/en/posts/s7-1500-mb-server-holding-register-max-word/224636
23. Siemens, *SIMATIC S7-1500 Technical Specifications* — CPU-specific DB size limits in each CPU manual's "Memory" table.
24. Siemens TIA Portal Online Docs, *Error messages (S7-1200, S7-1500) — Modbus instructions*. https://docs.tia.siemens.cloud/r/en-us/v20/modbus-rtu-s7-1200-s7-1500/error-messages-s7-1200-s7-1500
25. Industrial Monitor Direct, *Fix Siemens S7-1500 MB_Client UnitID Error 80C8*. https://industrialmonitordirect.com/blogs/knowledgebase/troubleshooting-mb-client-on-s7-1500-cpu-1515sp-modbus-tcp
26. Siemens SiePortal forum, *How many TCP connections can the S7-1200 make?*, Entry ID 275570. https://support.industry.siemens.com/forum/WW/en/posts/how-many-tcp-connections-can-the-s7-1200-make/275570
27. Siemens SiePortal forum, *Simultaneous connections of Modbus TCP*, Entry ID 189626. https://support.industry.siemens.com/forum/ww/en/posts/simultaneous-connections-of-modbus-tcp/189626
28. Siemens SiePortal forum, *How many Modbus TCP IP clients can read simultaneously from S7-1517*, Entry ID 261569. https://support.industry.siemens.com/forum/WW/en/posts/how-many-modbus-tcp-ip-client-can-read-simultaneously-in-s7-1517/261569
29. Industrial Monitor Direct, *Troubleshooting Intermittent Modbus TCP Connections on S7-1500 PLC*. https://industrialmonitordirect.com/blogs/knowledgebase/troubleshooting-intermittent-modbus-tcp-connections-on-s7-1500-plc
30. PLCtalk forum *"S7-1500 modbus tcp speed?"*. https://www.plctalk.net/forums/threads/s7-1500-modbus-tcp-speed.114046/
31. Siemens SiePortal forum, *MB_Unit_ID parameter in Modbus TCP*, Entry ID 156635. https://support.industry.siemens.com/forum/WW/en/posts/mb-unit-id-parameter-in-modbus-tcp/156635

View File

@@ -0,0 +1,43 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.Mitsubishi;
/// <summary>
/// Tag map for the Mitsubishi MELSEC device class with a representative Modbus Device
/// Assignment block mapping D0..D1023 → HR[0..1023]. Mirrors the behaviors in
/// <c>mitsubishi.json</c> pymodbus profile and <c>docs/v2/mitsubishi.md</c>.
/// </summary>
/// <remarks>
/// MELSEC Modbus sites all have *different* device-assignment parameter blocks; this profile
/// models the conventional default. Per-model differences (FX5U needs firmware ≥ 1.060 for
/// Modbus server; QJ71MT91 lacks FC22/FC23; FX/iQ-F use octal X/Y while Q/L/iQ-R use hex)
/// are handled in <see cref="MelsecAddress"/> (PR 59) and the per-model test files.
/// </remarks>
public static class MitsubishiProfile
{
/// <summary>
/// Scratch HR the smoke test writes + reads. Address 200 mirrors the
/// dl205/s7_1500/standard scratch range so one smoke test pattern works across every
/// device profile the simulator supports.
/// </summary>
public const ushort SmokeHoldingRegister = 200;
/// <summary>Value the smoke test writes then reads back.</summary>
public const short SmokeHoldingValue = 7890;
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),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
}

View File

@@ -0,0 +1,45 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.Mitsubishi;
/// <summary>
/// End-to-end smoke against the MELSEC <c>mitsubishi.json</c> pymodbus profile (or a real
/// MELSEC QJ71MT91 / iQ-R / FX5U when <c>MODBUS_SIM_ENDPOINT</c> points at one). Drives
/// the full <see cref="ModbusDriver"/> + real <see cref="ModbusTcpTransport"/> stack.
/// Success proves the driver initializes against the MELSEC sim, writes a known value,
/// and reads it back — the baseline every Mitsubishi-specific test (PR 59+) builds on.
/// </summary>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "Mitsubishi")]
public sealed class MitsubishiSmokeTests(ModbusSimulatorFixture sim)
{
[Fact]
public async Task Mitsubishi_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"), "mitsubishi",
StringComparison.OrdinalIgnoreCase))
{
Assert.Skip("MODBUS_SIM_PROFILE != mitsubishi — skipping.");
}
var options = MitsubishiProfile.BuildOptions(sim.Host, sim.Port);
await using var driver = new ModbusDriver(options, driverInstanceId: "melsec-smoke");
await driver.InitializeAsync(driverConfigJson: "{}", TestContext.Current.CancellationToken);
var writeResults = await driver.WriteAsync(
[new(FullReference: "Smoke_HReg200", Value: (short)MitsubishiProfile.SmokeHoldingValue)],
TestContext.Current.CancellationToken);
writeResults.Count.ShouldBe(1);
writeResults[0].StatusCode.ShouldBe(0u, "write must succeed against the MELSEC pymodbus 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)MitsubishiProfile.SmokeHoldingValue);
}
}

View File

@@ -0,0 +1,83 @@
{
"_comment": "mitsubishi.json -- Mitsubishi MELSEC Modbus TCP quirk simulator covering QJ71MT91, iQ-R, iQ-F/FX5U, and FX3U-ENET-P502 behaviors documented in docs/v2/mitsubishi.md. MELSEC CPUs store multi-word values in CDAB order (opposite of S7 ABCD, same family as DL260). The Modbus-module 'Modbus Device Assignment Parameter' block is per-site, so this profile models one *representative* assignment mapping D-register D0..D1023 -> HR 0..1023, M-relay M0..M511 -> coil 0..511, X-input X0..X15 -> DI 0..15 (X-addresses are HEX on Q/L/iQ-R, so X10 = decimal 16; on FX/iQ-F they're OCTAL like DL260). pymodbus bit-address semantics are the same as dl205.json and s7_1500.json (FC01/02/05/15 address N maps to cell index N/16).",
"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],
[10, 10],
[100, 101],
[200, 209],
[300, 301],
[500, 500]
],
"uint16": [
{"_quirk": "D0 fingerprint marker. MELSEC D0 is the first data register; Modbus Device Assignment typically maps D0..D1023 -> HR 0..1023. 0x1234 is the fingerprint operators set in GX Works to prove the mapping parameter block is in effect.",
"addr": 0, "value": 4660},
{"_quirk": "Scratch HR range 200..209 -- mirrors the dl205/s7_1500/standard scratch range so smoke tests (MitsubishiProfile.SmokeHoldingRegister=200) round-trip identically against any 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 CDAB word order (MELSEC Q/L/iQ-R/iQ-F default, same as DL260). HR[100]=0x0000=0 low word, HR[101]=0x3FC0=16320 high word. Decode with ByteOrder.WordSwap returns 1.5f; BigEndian decode returns a denormal.",
"addr": 100, "value": 0},
{"addr": 101, "value": 16320},
{"_quirk": "Int32 0x12345678 in CDAB word order. HR[300]=0x5678=22136 low word, HR[301]=0x1234=4660 high word. Contrasts with the S7 profile's ABCD encoding at the same address.",
"addr": 300, "value": 22136},
{"addr": 301, "value": 4660},
{"_quirk": "D10 = decimal 1234 stored as BINARY (NOT BCD like DL205). 0x04D2 = 1234 decimal. Caller reading with Bcd16 data type would decode this as binary 1234's BCD nibbles which are non-BCD and throw InvalidDataException -- proves MELSEC is binary-by-default, opposite of DL205's BCD-by-default quirk.",
"addr": 10, "value": 1234},
{"_quirk": "Modbus Device Assignment boundary marker. HR[500] represents the last register in an assigned D-range D500. Beyond this (HR[501..4095]) would be Illegal Data Address on a real QJ71MT91 with this specific parameter block; pymodbus returns default 0 because its shared cell array has space -- real-PLC parity is documented in docs/v2/mitsubishi.md §device-assignment, not enforced here.",
"addr": 500, "value": 500}
],
"bits": [
{"_quirk": "M-relay marker cell at cell 32 = Modbus coil 512 = MELSEC M512 (coils 0..15 collide with the D0 uint16 marker cell, so we place the M marker above that). Cell 32 bit 0 = 1 and bit 2 = 1 (value = 0b101 = 5) = M512=ON, M513=OFF, M514=ON. Matches the Y0/Y2 marker pattern in dl205 and s7_1500 profiles.",
"addr": 32, "value": 5},
{"_quirk": "X-input marker cell at cell 33 = Modbus DI 528 (= MELSEC X210 hex on Q/L/iQ-R). Cell 33 bit 0 = 1 and bit 3 = 1 (value = 0x9 = 9). Chosen above cell 1 so it doesn't collide with any uint16 D-register. Proves the hex-parsing X-input helper on Q/L/iQ-R family; FX/iQ-F families use octal X-addresses tested separately.",
"addr": 33, "value": 9}
],
"uint32": [],
"float32": [],
"string": [],
"repeat": []
}
}
}

View File

@@ -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": []
}
}
}

View File

@@ -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
)

View File

@@ -0,0 +1,44 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7;
/// <summary>
/// Tag map for the Siemens SIMATIC S7-1500 device class with the <c>MB_SERVER</c> library
/// block mapping HR[0..] to DB1.DBW0+. Mirrors <c>s7_1500.json</c> in <c>Pymodbus/</c>.
/// </summary>
/// <remarks>
/// 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
/// <c>docs/v2/s7.md</c> §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.
/// </remarks>
public static class S7_1500Profile
{
/// <summary>
/// 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.
/// </summary>
public const ushort SmokeHoldingRegister = 200;
/// <summary>Value the smoke test writes then reads back.</summary>
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 },
};
}

View File

@@ -0,0 +1,54 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7;
/// <summary>
/// End-to-end smoke against the S7-1500 <c>MB_SERVER</c> pymodbus profile (or a real
/// S7-1500 + MB_SERVER deployment when <c>MODBUS_SIM_ENDPOINT</c> points at one). Drives
/// the full <see cref="ModbusDriver"/> + real <see cref="ModbusTcpTransport"/> 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.
/// </summary>
/// <remarks>
/// 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
/// (<c>--filter DisplayName~S7</c>) should surface the quirk-specific failure mode when
/// something goes wrong, not a blanket smoke failure that could mean anything.
/// </remarks>
[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);
}
}

View File

@@ -0,0 +1,132 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.S7;
/// <summary>
/// Verifies the Siemens S7 big-endian (<c>ABCD</c>) word-order default for Float32 and
/// Int32 against the <c>s7_1500.json</c> pymodbus profile. S7's native CPU types are
/// big-endian end-to-end, so <c>MB_SERVER</c> places the high word at the lower register
/// address — <b>opposite</b> of DL260's CDAB. The driver's S7-family tag config must
/// therefore default to <see cref="ModbusByteOrder.BigEndian"/>; selecting
/// <see cref="ModbusByteOrder.WordSwap"/> against an S7 would decode garbage.
/// </summary>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "S7")]
public sealed class S7_ByteOrderTests(ModbusSimulatorFixture sim)
{
[Fact]
public async Task S7_Float32_ABCD_decodes_1_5f_from_HR100()
{
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 (s7_1500 profile is the only one seeding HR[100..101] ABCD).");
}
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition("S7_Float_ABCD",
ModbusRegion.HoldingRegisters, Address: 100,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.BigEndian),
// Control: same address with WordSwap should decode garbage — proves the
// two code paths diverge on S7 wire bytes.
new ModbusTagDefinition("S7_Float_CDAB_control",
ModbusRegion.HoldingRegisters, Address: 100,
DataType: ModbusDataType.Float32, Writable: false,
ByteOrder: ModbusByteOrder.WordSwap),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-float-abcd");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(
["S7_Float_ABCD", "S7_Float_CDAB_control"],
TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe(1.5f, "S7 MB_SERVER stores Float32 in ABCD word order; BigEndian decode returns 1.5f");
results[1].StatusCode.ShouldBe(0u);
results[1].Value.ShouldNotBe(1.5f, "applying CDAB swap to S7 ABCD bytes must produce a different value — confirms the flag is not a no-op and S7 profile default must be BigEndian");
}
[Fact]
public async Task S7_Int32_ABCD_decodes_0x12345678_from_HR300()
{
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.");
}
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition("S7_Int32_ABCD",
ModbusRegion.HoldingRegisters, Address: 300,
DataType: ModbusDataType.Int32, Writable: false,
ByteOrder: ModbusByteOrder.BigEndian),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-int-abcd");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(["S7_Int32_ABCD"], TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe(0x12345678,
"S7 Int32 stored as HR[300]=0x1234, HR[301]=0x5678 with ABCD order decodes to 0x12345678 — DL260 would store the reverse order");
}
[Fact]
public async Task S7_DB1_fingerprint_marker_at_HR0_reads_0xABCD()
{
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.");
}
// Real-world MB_SERVER deployments typically reserve DB1.DBW0 as a fingerprint so
// clients can verify they're pointing at the right DB (protects against typos in
// the MB_SERVER.MB_HOLD_REG.DB_number parameter). 0xABCD is the convention.
var options = new ModbusDriverOptions
{
Host = sim.Host,
Port = sim.Port,
UnitId = 1,
Timeout = TimeSpan.FromSeconds(2),
Tags =
[
new ModbusTagDefinition("S7_Fingerprint",
ModbusRegion.HoldingRegisters, Address: 0,
DataType: ModbusDataType.UInt16, Writable: false),
],
Probe = new ModbusProbeOptions { Enabled = false },
};
await using var driver = new ModbusDriver(options, driverInstanceId: "s7-fingerprint");
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var results = await driver.ReadAsync(["S7_Fingerprint"], TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0u);
results[0].Value.ShouldBe((ushort)0xABCD);
}
}

View File

@@ -26,6 +26,8 @@
<ItemGroup>
<None Update="Pymodbus\**\*" CopyToOutputDirectory="PreserveNewest"/>
<None Update="DL205\**\*" CopyToOutputDirectory="PreserveNewest"/>
<None Update="S7\**\*" CopyToOutputDirectory="PreserveNewest"/>
<None Update="Mitsubishi\**\*" CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
<ItemGroup>