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>
99 lines
4.6 KiB
JSON
99 lines
4.6 KiB
JSON
{
|
|
"_comment": "DL205.json — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator. Models each behavior in docs/v2/dl205.md as concrete register values so DL205_<behavior> integration tests can assert against this profile WITHOUT a live PLC. Loaded by `pymodbus.simulator`. See ../README.md. Per-quirk address layout matches the table in dl205.md exactly. `shared blocks: true` matches DL series behavior — coils/HR overlay the same word address space (a Y-output is both a discrete bit AND part of a system V-memory register).",
|
|
|
|
"server_list": {
|
|
"srv": {
|
|
"comm": "tcp",
|
|
"host": "0.0.0.0",
|
|
"port": 5020,
|
|
"framer": "socket",
|
|
"device_id": 1
|
|
}
|
|
},
|
|
|
|
"device_list": {
|
|
"dev": {
|
|
"setup": {
|
|
"co size": 16384,
|
|
"di size": 8192,
|
|
"hr size": 16384,
|
|
"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, 16383]
|
|
],
|
|
|
|
"_comment_uint16": "Holding-register seeds. Every quirky value is a raw uint16 with the byte math worked out in dl205.md so the simulator serves it verbatim — pymodbus does NOT decode strings, BCD, or float-CDAB on its own; that's the driver's job.",
|
|
|
|
"uint16": [
|
|
{"_quirk": "V0 marker. HR[0] = 0xCAFE proves register 0 is valid on DL205/DL260 (rejects-register-0 was a DL05/DL06 relative-mode artefact). 0xCAFE = 51966.",
|
|
"addr": 0, "value": 51966},
|
|
|
|
{"_quirk": "V2000 marker. V2000 octal = decimal 1024 = PDU 0x0400. Marker 0x2000 = 8192.",
|
|
"addr": 1024, "value": 8192},
|
|
|
|
{"_quirk": "V40400 marker. V40400 octal = decimal 8448 = PDU 0x2100 (NOT register 0). Marker 0x4040 = 16448.",
|
|
"addr": 8448, "value": 16448},
|
|
|
|
{"_quirk": "String 'Hello' first char in LOW byte. HR[0x410] = 'H'(0x48) lo + 'e'(0x65) hi = 0x6548 = 25928.",
|
|
"addr": 1040, "value": 25928},
|
|
{"_quirk": "String 'Hello' second char-pair: 'l'(0x6C) lo + 'l'(0x6C) hi = 0x6C6C = 27756.",
|
|
"addr": 1041, "value": 27756},
|
|
{"_quirk": "String 'Hello' third char-pair: 'o'(0x6F) lo + null(0x00) hi = 0x006F = 111.",
|
|
"addr": 1042, "value": 111},
|
|
|
|
{"_quirk": "Float32 1.5f in CDAB word order. IEEE 754 1.5 = 0x3FC00000. CDAB = low word first: HR[0x420]=0x0000, HR[0x421]=0x3FC0=16320.",
|
|
"addr": 1056, "value": 0},
|
|
{"_quirk": "Float32 1.5f CDAB high word.",
|
|
"addr": 1057, "value": 16320},
|
|
|
|
{"_quirk": "BCD register. Decimal 1234 stored as BCD nibbles 0x1234 = 4660. NOT binary 1234 (= 0x04D2).",
|
|
"addr": 1072, "value": 4660},
|
|
|
|
{"_quirk": "FC03 cap test. Real DL205/DL260 FC03 caps at 128 registers (above spec's 125). HR[1280..1407] is 128 contiguous registers; rest of block defaults to 0.",
|
|
"addr": 1280, "value": 0},
|
|
{"addr": 1281, "value": 1},
|
|
{"addr": 1282, "value": 2},
|
|
{"addr": 1343, "value": 63, "_marker": "FC03Block_mid"},
|
|
{"addr": 1407, "value": 127, "_marker": "FC03Block_last"}
|
|
],
|
|
|
|
"_comment_bits": "Coils — Y outputs at 2048+, C relays at 3072+, scratch C at 4000-4007 for write tests. DL260 X inputs would be at discrete-input addresses 0..511 but pymodbus's shared-blocks mode + same-table-as-coils means those would conflict with HR seeds; FC02 tests against this profile use a separate discrete-input block instead — that's why `di size` is large but the X-input markers live in `bits` only when `shared blocks=false`. Document trade-off in README.",
|
|
|
|
"bits": [
|
|
{"_quirk": "Y0 marker. DL260 maps Y0 to coil 2048 (0-based). Coil 2048 = ON proves the mapping.",
|
|
"addr": 2048, "value": 1},
|
|
{"addr": 2049, "value": 0},
|
|
{"addr": 2050, "value": 1},
|
|
|
|
{"_quirk": "C0 marker. DL260 maps C0 to coil 3072 (0-based). Coil 3072 = ON proves the mapping.",
|
|
"addr": 3072, "value": 1},
|
|
{"addr": 3073, "value": 0},
|
|
{"addr": 3074, "value": 1},
|
|
|
|
{"_quirk": "Scratch C-relays for write-roundtrip tests against the writable C range.",
|
|
"addr": 4000, "value": 0},
|
|
{"addr": 4001, "value": 0},
|
|
{"addr": 4002, "value": 0},
|
|
{"addr": 4003, "value": 0},
|
|
{"addr": 4004, "value": 0},
|
|
{"addr": 4005, "value": 0},
|
|
{"addr": 4006, "value": 0},
|
|
{"addr": 4007, "value": 0}
|
|
],
|
|
|
|
"uint32": [],
|
|
"float32": [],
|
|
"string": [],
|
|
"repeat": []
|
|
}
|
|
}
|
|
}
|