ModbusSimulatorFixture default port bumped from 502 to 5020 to match the pymodbus convention. Override via MODBUS_SIM_ENDPOINT for a real PLC on its native 502. Skip-message updated to point at the new Pymodbus\serve.ps1 wrapper instead of 'start ModbusPal'. csproj <None Update> rule swapped from ModbusPal/** to Pymodbus/** so the new JSON profiles + serve.ps1 + README copy to test-output as PreserveNewest.
standard.json — generic Modbus TCP server, slave id 1, port 5020, shared blocks=false (independent coils + HR address spaces, more textbook-PLC-like). HR[0..31] seeded with address-as-value via per-register uint16 entries, HR[100] auto-increments via the built-in increment action with parameters minval=0/maxval=65535 (drives subscribe-and-receive integration tests so they have a register that ticks without a write — pymodbus's increment ticks per-access not wall-clock, which is good enough for a 250ms-poll test), HR[200..209] scratch range left at 0 for write tests, coils 0..31 alternating, coils 100..109 scratch. write list covers 0..1023 so any test address is mutable.
dl205.json — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator, slave id 1, port 5020, shared blocks=true (matches DL series memory model where coils/DI/HR overlay the same word address space). Each quirky register seeded with the pre-computed raw uint16 value documented in docs/v2/dl205.md, with an inline _quirk JSON-comment naming the behavior so future-me reading the file knows why HR[1040]=25928 means 'H' lo / 'e' hi (the user's headline string-byte-order finding). Encoded quirks: V0 marker at HR[0]=0xCAFE; V2000 at HR[1024]=0x2000; V40400 at HR[8448]=0x4040; 'Hello' string at HR[1040..1042] first-char-low-byte; Float32 1.5f at HR[1056..1057] in CDAB word order (low word first); BCD register at HR[1072]=0x1234; FC03-128-cap block at HR[1280..1407]; Y0/C0 coil markers at 2048/3072; scratch C-relays at 4000..4007.
serve.ps1 wrapper — pwsh script with a -Profile {standard|dl205} parameter switch. Validates pymodbus.simulator is on PATH (clearer message than the raw CommandNotFoundException), validates the profile JSON exists, builds the right --modbus_server/--modbus_device/--json_file/--http_port arg list, and execs pymodbus.simulator in the foreground. -HttpPort 0 disables the web UI. Foreground exec lets the operator Ctrl+C to stop without an extra control script.
README.md fully rewritten for pymodbus: install command (pip install 'pymodbus[simulator]==3.13.0' — pinned for reproducibility, [simulator] extra pulls aiohttp), per-profile reference tables, the same DL205 quirk → register table from PR 42 but adjusted for pymodbus paths, what's-NEW-vs-ModbusPal section (all four tables, raw uint16 seeding, declarative actions, custom Python action modules, headless, web UI, maintained), trade-offs section (float32-as-two-uint16s for explicit CDAB control, increment ticks per-access not wall-clock, shared-blocks mode for DL205 vs separate for Standard), file-format quick reference for hand-authoring more profiles. References pinned to the pymodbus readthedocs simulator/config + REST API pages.
docs/v2/modbus-test-plan.md harness section rewritten with the swap rationale; PR-history list updated to mark PR 42 SUPERSEDED by PR 43 and call out PR 44+ as the per-quirk implementation track. Test-conventions bullet about 'don't depend on ModbusPal state between tests' generalized to 'don't depend on simulator state' and a note added that pymodbus's REST API can reset state between facts if a test ever needs it.
DL205Profile.cs and DL205SmokeTests.cs xml-doc updated to reference pymodbus / dl205.json instead of ModbusPal / DL205.xmpp.
Functional validation deferred — Python isn't installed on this dev box (winget search returned no matches for Python.Python.3 exact). JSON parses structurally (PowerShell ConvertFrom-Json clean on both files), build clean, .json + serve.ps1 + README all copy to test-output as expected. User installs pymodbus when they want to actually run the simulator end-to-end; if pymodbus rejects the config the README's reference link to pymodbus's simulator/config schema doc is the right next stop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
3.4 KiB
JSON
82 lines
3.4 KiB
JSON
{
|
|
"_comment": "Standard.json — generic Modbus TCP server for the integration suite. Loaded by `pymodbus.simulator`. See ../README.md for the launch command. Holding registers 0..31 are seeded with their address as value (HR[5]=5) for easy mental-map diagnostics. HR[100] auto-increments via pymodbus's built-in `increment` action so subscribe-and-receive integration tests have a register that ticks without a write. HR[200..209] is a scratch range left at 0 for write-roundtrip tests. Coils 0..31 alternate on/off (even=on); coils 100..109 scratch.",
|
|
|
|
"server_list": {
|
|
"srv": {
|
|
"comm": "tcp",
|
|
"host": "0.0.0.0",
|
|
"port": 5020,
|
|
"framer": "socket",
|
|
"device_id": 1
|
|
}
|
|
},
|
|
|
|
"device_list": {
|
|
"dev": {
|
|
"setup": {
|
|
"co size": 1024,
|
|
"di size": 1024,
|
|
"hr size": 1024,
|
|
"ir size": 1024,
|
|
"shared blocks": false,
|
|
"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, 1023]
|
|
],
|
|
|
|
"bits": [
|
|
{"addr": 0, "value": 1}, {"addr": 1, "value": 0},
|
|
{"addr": 2, "value": 1}, {"addr": 3, "value": 0},
|
|
{"addr": 4, "value": 1}, {"addr": 5, "value": 0},
|
|
{"addr": 6, "value": 1}, {"addr": 7, "value": 0},
|
|
{"addr": 8, "value": 1}, {"addr": 9, "value": 0},
|
|
{"addr": 10, "value": 1}, {"addr": 11, "value": 0},
|
|
{"addr": 12, "value": 1}, {"addr": 13, "value": 0},
|
|
{"addr": 14, "value": 1}, {"addr": 15, "value": 0},
|
|
{"addr": 16, "value": 1}, {"addr": 17, "value": 0},
|
|
{"addr": 18, "value": 1}, {"addr": 19, "value": 0},
|
|
{"addr": 20, "value": 1}, {"addr": 21, "value": 0},
|
|
{"addr": 22, "value": 1}, {"addr": 23, "value": 0},
|
|
{"addr": 24, "value": 1}, {"addr": 25, "value": 0},
|
|
{"addr": 26, "value": 1}, {"addr": 27, "value": 0},
|
|
{"addr": 28, "value": 1}, {"addr": 29, "value": 0},
|
|
{"addr": 30, "value": 1}, {"addr": 31, "value": 0}
|
|
],
|
|
|
|
"uint16": [
|
|
{"addr": 0, "value": 0}, {"addr": 1, "value": 1},
|
|
{"addr": 2, "value": 2}, {"addr": 3, "value": 3},
|
|
{"addr": 4, "value": 4}, {"addr": 5, "value": 5},
|
|
{"addr": 6, "value": 6}, {"addr": 7, "value": 7},
|
|
{"addr": 8, "value": 8}, {"addr": 9, "value": 9},
|
|
{"addr": 10, "value": 10}, {"addr": 11, "value": 11},
|
|
{"addr": 12, "value": 12}, {"addr": 13, "value": 13},
|
|
{"addr": 14, "value": 14}, {"addr": 15, "value": 15},
|
|
{"addr": 16, "value": 16}, {"addr": 17, "value": 17},
|
|
{"addr": 18, "value": 18}, {"addr": 19, "value": 19},
|
|
{"addr": 20, "value": 20}, {"addr": 21, "value": 21},
|
|
{"addr": 22, "value": 22}, {"addr": 23, "value": 23},
|
|
{"addr": 24, "value": 24}, {"addr": 25, "value": 25},
|
|
{"addr": 26, "value": 26}, {"addr": 27, "value": 27},
|
|
{"addr": 28, "value": 28}, {"addr": 29, "value": 29},
|
|
{"addr": 30, "value": 30}, {"addr": 31, "value": 31},
|
|
|
|
{"addr": 100, "value": 0,
|
|
"action": "increment",
|
|
"parameters": {"minval": 0, "maxval": 65535}}
|
|
],
|
|
|
|
"uint32": [],
|
|
"float32": [],
|
|
"string": [],
|
|
"repeat": []
|
|
}
|
|
}
|
|
}
|