Compare commits
6 Commits
phase-3-pr
...
phase-3-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f52b7d8979 | ||
|
|
b54724a812 | ||
|
|
10c724b5b6 | ||
| 8c89d603e8 | |||
| 299bd4a932 | |||
|
|
9e2b5b330f |
1
_p54.json
Normal file
1
_p54.json
Normal 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
1
_p55.json
Normal 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
485
docs/v2/s7.md
Normal 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 0–8191) [1][9] |
|
||||
| Discrete Inputs (1x)| FC02 | *implicit* — I process image | `%I0.0`–`%I1023.7` (→ discrete addresses 0–8191) [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
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user