virtual-plc 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- virtual_plc-0.3.0/AGENTS.md +35 -0
- virtual_plc-0.3.0/LICENSE +21 -0
- virtual_plc-0.3.0/MANIFEST.in +4 -0
- virtual_plc-0.3.0/PKG-INFO +249 -0
- virtual_plc-0.3.0/README.md +219 -0
- virtual_plc-0.3.0/docs/BEHAVIOR.md +577 -0
- virtual_plc-0.3.0/docs/DESIGN.md +257 -0
- virtual_plc-0.3.0/docs/FAULTS.md +139 -0
- virtual_plc-0.3.0/llms.txt +27 -0
- virtual_plc-0.3.0/pyproject.toml +49 -0
- virtual_plc-0.3.0/requirements-dev.txt +2 -0
- virtual_plc-0.3.0/setup.cfg +4 -0
- virtual_plc-0.3.0/tests/conftest.py +87 -0
- virtual_plc-0.3.0/tests/data/scenarios.jsonl +10 -0
- virtual_plc-0.3.0/tests/oracle_pin.txt +4 -0
- virtual_plc-0.3.0/tests/tables.py +146 -0
- virtual_plc-0.3.0/tests/test_ascii_dword_order.py +136 -0
- virtual_plc-0.3.0/tests/test_client_coverage.py +109 -0
- virtual_plc-0.3.0/tests/test_guide.py +281 -0
- virtual_plc-0.3.0/tests/test_hardening_api_cli.py +214 -0
- virtual_plc-0.3.0/tests/test_hardening_protocol.py +245 -0
- virtual_plc-0.3.0/tests/test_m1_client.py +82 -0
- virtual_plc-0.3.0/tests/test_m1_codec.py +72 -0
- virtual_plc-0.3.0/tests/test_m1_memory.py +72 -0
- virtual_plc-0.3.0/tests/test_m1_server_raw.py +314 -0
- virtual_plc-0.3.0/tests/test_m2_client.py +84 -0
- virtual_plc-0.3.0/tests/test_m2_raw.py +160 -0
- virtual_plc-0.3.0/tests/test_m2b_devices.py +75 -0
- virtual_plc-0.3.0/tests/test_m3_m4_client.py +154 -0
- virtual_plc-0.3.0/tests/test_m3_m4_codec.py +55 -0
- virtual_plc-0.3.0/tests/test_m3_m4_raw.py +160 -0
- virtual_plc-0.3.0/tests/test_m5_control.py +192 -0
- virtual_plc-0.3.0/tests/test_m5_layouts.py +39 -0
- virtual_plc-0.3.0/tests/test_m5_memory_lock.py +138 -0
- virtual_plc-0.3.0/tests/test_m6_client.py +167 -0
- virtual_plc-0.3.0/tests/test_m6_profiles_memory.py +100 -0
- virtual_plc-0.3.0/tests/test_m6_wire.py +108 -0
- virtual_plc-0.3.0/tests/test_m7_client.py +78 -0
- virtual_plc-0.3.0/tests/test_m7_faults_raw.py +181 -0
- virtual_plc-0.3.0/tests/test_m7_plan_cli.py +126 -0
- virtual_plc-0.3.0/tests/test_oracle_pin.py +44 -0
- virtual_plc-0.3.0/tests/test_oracle_updates.py +20 -0
- virtual_plc-0.3.0/tests/test_release_cleanup.py +111 -0
- virtual_plc-0.3.0/tests/test_tables.py +62 -0
- virtual_plc-0.3.0/tests/test_text_pad.py +146 -0
- virtual_plc-0.3.0/virtual_plc/__init__.py +14 -0
- virtual_plc-0.3.0/virtual_plc/__main__.py +161 -0
- virtual_plc-0.3.0/virtual_plc/codec.py +235 -0
- virtual_plc-0.3.0/virtual_plc/commands.py +221 -0
- virtual_plc-0.3.0/virtual_plc/faults.py +139 -0
- virtual_plc-0.3.0/virtual_plc/guide.py +343 -0
- virtual_plc-0.3.0/virtual_plc/memory.py +145 -0
- virtual_plc-0.3.0/virtual_plc/profiles.py +111 -0
- virtual_plc-0.3.0/virtual_plc/server.py +191 -0
- virtual_plc-0.3.0/virtual_plc.egg-info/PKG-INFO +249 -0
- virtual_plc-0.3.0/virtual_plc.egg-info/SOURCES.txt +58 -0
- virtual_plc-0.3.0/virtual_plc.egg-info/dependency_links.txt +1 -0
- virtual_plc-0.3.0/virtual_plc.egg-info/entry_points.txt +2 -0
- virtual_plc-0.3.0/virtual_plc.egg-info/requires.txt +7 -0
- virtual_plc-0.3.0/virtual_plc.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Working on VirtualPLC
|
|
2
|
+
|
|
3
|
+
This file is for agents developing this repository. For product usage, run
|
|
4
|
+
`virtual-plc guide` or `.venv/Scripts/python.exe -m virtual_plc guide`.
|
|
5
|
+
|
|
6
|
+
## Workflow
|
|
7
|
+
|
|
8
|
+
- Windows venv: `.venv/Scripts/python.exe`; POSIX: `.venv/bin/python`.
|
|
9
|
+
- Install locally: `python -m pip install -e . -r requirements-dev.txt`.
|
|
10
|
+
- Behavior changes require TDD: add tests, observe RED, implement GREEN, update docs.
|
|
11
|
+
- Run `.venv/Scripts/python.exe -m pytest -q` before reporting completion.
|
|
12
|
+
- Tests bind port 0 and use the real installed pymcprotocol client where applicable.
|
|
13
|
+
- Keep runtime code small and stdlib only; generate transport/profile cases.
|
|
14
|
+
- On Windows sandbox temp ACL failures, a process-local mkdir mode workaround is
|
|
15
|
+
acceptable; remove temporary helpers afterward and report its use.
|
|
16
|
+
|
|
17
|
+
## Sources of truth
|
|
18
|
+
|
|
19
|
+
- `virtual_plc/__main__.py:build_parser` owns CLI flags, defaults and help.
|
|
20
|
+
- `virtual_plc/profiles.py` and command dispatch tables own profile/device/command data.
|
|
21
|
+
- `virtual_plc/guide.py` owns compact English usage prose; tables are generated.
|
|
22
|
+
- README marked `guide:run` recipes must match guide run (test-enforced).
|
|
23
|
+
- README is the human entry point; docs/BEHAVIOR.md and docs/FAULTS.md explain semantics.
|
|
24
|
+
- Update docs/DESIGN.md when design decisions or open items change.
|
|
25
|
+
- `requirements-dev.txt` is the only exact oracle pin; test_oracle_pin.py checks it.
|
|
26
|
+
- The wheel hash is checked only for its recorded version. Keep the full suite and
|
|
27
|
+
tests/test_client_coverage.py green after bumps; cover methods/constants/lookup names.
|
|
28
|
+
|
|
29
|
+
## Do not
|
|
30
|
+
|
|
31
|
+
- Do not commit or publish unless the user requests it.
|
|
32
|
+
- Do not add dependencies or duplicate option/device/command tables in guide prose.
|
|
33
|
+
- Do not label simulator policies as verified physical PLC behavior.
|
|
34
|
+
- Do not access direct Memory concurrently with wire requests; use clients.
|
|
35
|
+
- Do not expose the simulator to production networks or real equipment.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 greatgc-flow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: virtual_plc
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Local Mitsubishi MC protocol PLC simulator with configurable response faults
|
|
5
|
+
Author: greatgc-flow
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/greatgc-flow/VirtualPLC
|
|
8
|
+
Project-URL: Repository, https://github.com/greatgc-flow/VirtualPLC
|
|
9
|
+
Project-URL: Issues, https://github.com/greatgc-flow/VirtualPLC/issues
|
|
10
|
+
Keywords: plc,mitsubishi,mc-protocol,simulator,testing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest; extra == "test"
|
|
28
|
+
Requires-Dist: pymcprotocol>=0.3.0; extra == "test"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# VirtualPLC
|
|
32
|
+
|
|
33
|
+
A local Mitsubishi MC protocol PLC simulator for client tests, with sparse
|
|
34
|
+
memory, CPU control and declarative faults. Python 3.10+, standard library only.
|
|
35
|
+
|
|
36
|
+
AI agents: run `virtual-plc guide` for a compact usage guide; use `--json` for data.
|
|
37
|
+
|
|
38
|
+
## 30-second quickstart
|
|
39
|
+
|
|
40
|
+
Install with `pip install virtual-plc`, then choose a recipe:
|
|
41
|
+
|
|
42
|
+
<!-- guide:run -->
|
|
43
|
+
```text
|
|
44
|
+
virtual-plc --port 5000 --text-pad 0x20
|
|
45
|
+
virtual-plc --profile iQ-R --port 5001 --initial-state STOP
|
|
46
|
+
virtual-plc --init seed.json --fault-plan faults.json --dump-on-exit saved.json
|
|
47
|
+
```
|
|
48
|
+
<!-- /guide:run -->
|
|
49
|
+
|
|
50
|
+
The third recipe needs the JSON files shown below. From a checkout, install
|
|
51
|
+
with `python -m pip install -e .`. `python -m virtual_plc` is the equivalent
|
|
52
|
+
entry point. For more startup details, run `virtual-plc guide run`.
|
|
53
|
+
|
|
54
|
+
Install the client separately with `pip install pymcprotocol`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from pymcprotocol import Type3E
|
|
58
|
+
|
|
59
|
+
client = Type3E(plctype="Q")
|
|
60
|
+
client.connect("127.0.0.1", 5000)
|
|
61
|
+
try:
|
|
62
|
+
client.batchwrite_wordunits("D0", [123, -45])
|
|
63
|
+
assert client.batchread_wordunits("D0", 2) == [123, -45]
|
|
64
|
+
finally:
|
|
65
|
+
client.close()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For the iQ-R recipe (`virtual-plc --profile iQ-R --port 5001 --initial-state STOP`):
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from pymcprotocol import Type3E
|
|
72
|
+
|
|
73
|
+
client = Type3E(plctype="iQ-R")
|
|
74
|
+
try:
|
|
75
|
+
client.connect("127.0.0.1", 5001)
|
|
76
|
+
assert client.batchread_wordunits("D0", 1) == [0]
|
|
77
|
+
finally:
|
|
78
|
+
client.close()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use localhost only. Passwords simulate PLC locking; there is no network
|
|
82
|
+
authentication. Never connect to real equipment or production networks.
|
|
83
|
+
Non-local binds print a warning, not an access control.
|
|
84
|
+
|
|
85
|
+
## CLI reference
|
|
86
|
+
|
|
87
|
+
Run `virtual-plc guide options` for the option/default/meaning table generated
|
|
88
|
+
from the parser, or `virtual-plc --help`. For programs, use
|
|
89
|
+
`virtual-plc guide options --json`. Bad configuration exits 2 with a topic
|
|
90
|
+
hint; bind failures exit 1 without a traceback.
|
|
91
|
+
|
|
92
|
+
The stderr banner reports the bound address (including the assigned port for
|
|
93
|
+
`--port 0`), profile/state, idle timeout, text pad, dword order and fault plan.
|
|
94
|
+
Ctrl+C, SIGTERM and Windows SIGBREAK stop workers and save exit snapshots.
|
|
95
|
+
SIGBREAK needs a shared console; forced termination bypasses Python handlers.
|
|
96
|
+
`-v` logs request summaries and close reasons, excluding payloads/passwords.
|
|
97
|
+
|
|
98
|
+
| Guide topic | Contents |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `virtual-plc guide run` | Installation, startup and shutdown |
|
|
101
|
+
| `virtual-plc guide options` | All flags and defaults, generated from argparse |
|
|
102
|
+
| `virtual-plc guide client` | Client reads/writes, control and addressing |
|
|
103
|
+
| `virtual-plc guide profiles` | Generated command/frame/wire/profile/device matrix |
|
|
104
|
+
| `virtual-plc guide embed` | Python API and pytest fixture |
|
|
105
|
+
| `virtual-plc guide faults` | Plan schema, actions and three examples |
|
|
106
|
+
| `virtual-plc guide memory` | Seeding, snapshots, ranges and devices |
|
|
107
|
+
| `virtual-plc guide troubleshoot` | Symptoms and fixes |
|
|
108
|
+
| `virtual-plc guide divergences` | Simulator policies and unverified PLC behavior |
|
|
109
|
+
|
|
110
|
+
Every topic accepts `--json`. Unknown topics list the choices and exit 2.
|
|
111
|
+
|
|
112
|
+
## Profiles, frames and encodings
|
|
113
|
+
|
|
114
|
+
Commands support 3E/4E and binary/ASCII on all profiles. Q, L, QnA and iQ-L
|
|
115
|
+
share a wire class; iQ-R uses different descriptors/subcommands. Client
|
|
116
|
+
`plctype` and server `--profile` must agree for iQ-R. Inspect
|
|
117
|
+
`virtual-plc guide profiles --json` for the current matrix and device kinds.
|
|
118
|
+
|
|
119
|
+
This local recipe covers every profile/frame/wire combination:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from itertools import product
|
|
123
|
+
from pymcprotocol import Type3E, Type4E
|
|
124
|
+
from virtual_plc import Server
|
|
125
|
+
|
|
126
|
+
for profile in ("Q", "L", "QnA", "iQ-L", "iQ-R"):
|
|
127
|
+
with Server(profile=profile) as server:
|
|
128
|
+
for kind, wire in product((Type3E, Type4E), ("binary", "ascii")):
|
|
129
|
+
client = kind(plctype=profile)
|
|
130
|
+
client.setaccessopt(commtype=wire)
|
|
131
|
+
client.connect(*server.address)
|
|
132
|
+
try:
|
|
133
|
+
client.batchwrite_wordunits("D0", [123])
|
|
134
|
+
assert client.batchread_wordunits("D0", 1) == [123]
|
|
135
|
+
finally:
|
|
136
|
+
client.close()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Memory seeding, snapshots and embedding
|
|
140
|
+
|
|
141
|
+
Save seed.json (decimal address keys, unsigned native point values):
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{"D": {"100": [1, 2, 3]}, "M": {"0": [1, 0, 1]}}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`--init` loads before listening; `--dump-on-exit` saves the same schema after
|
|
148
|
+
graceful shutdown. Dumps preserve explicit zeros and include SD203. Loads
|
|
149
|
+
validate the complete snapshot before overlaying data. See
|
|
150
|
+
`virtual-plc guide memory` for bounds, retention and device names.
|
|
151
|
+
|
|
152
|
+
Latch ranges are cleared by latchclear and preserved by remote_run(clear_mode=1).
|
|
153
|
+
SM and SD are never cleared; latchclear requires STOP.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from virtual_plc import Server, Memory, FaultPlan, __version__
|
|
157
|
+
|
|
158
|
+
memory = Memory()
|
|
159
|
+
memory.load({"D": {"100": [1, 2, 3]}})
|
|
160
|
+
with Server(memory=memory, fault_plan=FaultPlan([])) as server:
|
|
161
|
+
print(server.address, __version__)
|
|
162
|
+
# Connect clients here. Context exit stops and joins workers.
|
|
163
|
+
snapshot = memory.dump()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Seed before start/context entry; dump after stop. Direct Memory calls are not
|
|
167
|
+
synchronized with requests. Use clients for live writes. Server construction
|
|
168
|
+
initializes SD203; CLI seeding runs afterward. Writing SD203 changes the word
|
|
169
|
+
without changing CPU state. For a pytest fixture, run `virtual-plc guide embed`.
|
|
170
|
+
|
|
171
|
+
## Response faults
|
|
172
|
+
|
|
173
|
+
Save faults.json and pass `--fault-plan faults.json`:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{"steps": [{"match": {"command": "0x0619"}, "times": 1,
|
|
177
|
+
"action": {"type": "delay", "seconds": 0.02}}]}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Faults run after execution and do not roll back writes. Run
|
|
181
|
+
`virtual-plc guide faults` for the schema and examples; see
|
|
182
|
+
[FAULTS](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/FAULTS.md)
|
|
183
|
+
for delivery semantics.
|
|
184
|
+
|
|
185
|
+
## Troubleshooting
|
|
186
|
+
|
|
187
|
+
- Large reads: the client calls `recv(4096)` once. Split batches; the server's
|
|
188
|
+
point limit is separate. A raw reader can follow response lengths.
|
|
189
|
+
- CPU names: default Q returns `"VirtualPLC-Q\x00\x00\x00\x00"`. The client
|
|
190
|
+
retains NULs; `--text-pad 0x20` returns `"VirtualPLC-Q"`. Non-UTF-8 fill
|
|
191
|
+
such as 0xFF causes client decoding failure.
|
|
192
|
+
- Reset requires STOP. On receive/status failure, the client waits about 1 s
|
|
193
|
+
and reconnects. A normal reset keeps the connection open.
|
|
194
|
+
- Truncated or closed binary replies can look like status/value 0 because
|
|
195
|
+
the client does not validate headers/lengths. This is not proof of success.
|
|
196
|
+
- Idle timeout is disabled by default; explicit timeouts close idle or
|
|
197
|
+
incomplete connections. Reconnect afterward.
|
|
198
|
+
|
|
199
|
+
More fixes: `virtual-plc guide troubleshoot`. Protocol details:
|
|
200
|
+
[BEHAVIOR](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/BEHAVIOR.md).
|
|
201
|
+
|
|
202
|
+
## Known divergences from a real PLC
|
|
203
|
+
|
|
204
|
+
- CPU identities, ranges and SD203 state encoding are synthetic; SD203 mirrors
|
|
205
|
+
state in one direction. CPU names default to NUL fill on both encodings;
|
|
206
|
+
the client retains NULs. Physical fill remains unverified.
|
|
207
|
+
- ASCII addresses use decimal text even for hex-base devices. Dword order
|
|
208
|
+
defaults to low-word-first (`swapped`); `client` selects high-word-first.
|
|
209
|
+
Physical order is unverified; roundtrips remain symmetric.
|
|
210
|
+
- Locking is server-wide, survives disconnect and gates only CPU control.
|
|
211
|
+
It provides no authentication. Native iQ-R dword stride is one 32-bit point
|
|
212
|
+
per address; this and physical lock/reset policies are unverified.
|
|
213
|
+
- Timers/counters store values without execution. Monitoring timers are
|
|
214
|
+
parsed/ignored; routes alias one memory/state. Error meanings await checks.
|
|
215
|
+
|
|
216
|
+
Run `virtual-plc guide divergences` for the compact list of simulator policies.
|
|
217
|
+
|
|
218
|
+
## Development and releases
|
|
219
|
+
|
|
220
|
+
```text
|
|
221
|
+
python -m pip install -e . -r requirements-dev.txt
|
|
222
|
+
python -m pytest -q
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The dev extra installs pytest only. The test extra adds the compatible oracle
|
|
226
|
+
range declared in pyproject.toml. requirements-dev.txt is the only exact client
|
|
227
|
+
pin. Dependabot updates that file and pyproject; the coverage guard rejects
|
|
228
|
+
newly uncovered public methods, device constants and lookup-only names.
|
|
229
|
+
|
|
230
|
+
The [wheel hash](https://github.com/greatgc-flow/VirtualPLC/blob/main/tests/oracle_pin.txt)
|
|
231
|
+
is checked when a matching-version wheel is retained under .venv/oracle. A
|
|
232
|
+
bumped oracle skips only the stale artifact-hash check; the full suite and
|
|
233
|
+
coverage guard still run. Oracle watch installs the latest client and changes
|
|
234
|
+
only that run's local version expectation, without committing the change.
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
python -m pip install build twine
|
|
238
|
+
python -m build
|
|
239
|
+
python -m twine check dist/*
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
CI tests Windows/Linux on Python 3.10–3.13 and installs built wheels in clean
|
|
243
|
+
environments. Tags must match pyproject version; trusted publishing requires
|
|
244
|
+
the PyPI publisher and GitHub environment pypi to be configured.
|
|
245
|
+
|
|
246
|
+
See [DESIGN](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/DESIGN.md),
|
|
247
|
+
[BEHAVIOR](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/BEHAVIOR.md)
|
|
248
|
+
and [FAULTS](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/FAULTS.md).
|
|
249
|
+
Licensed under [MIT](https://github.com/greatgc-flow/VirtualPLC/blob/main/LICENSE).
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# VirtualPLC
|
|
2
|
+
|
|
3
|
+
A local Mitsubishi MC protocol PLC simulator for client tests, with sparse
|
|
4
|
+
memory, CPU control and declarative faults. Python 3.10+, standard library only.
|
|
5
|
+
|
|
6
|
+
AI agents: run `virtual-plc guide` for a compact usage guide; use `--json` for data.
|
|
7
|
+
|
|
8
|
+
## 30-second quickstart
|
|
9
|
+
|
|
10
|
+
Install with `pip install virtual-plc`, then choose a recipe:
|
|
11
|
+
|
|
12
|
+
<!-- guide:run -->
|
|
13
|
+
```text
|
|
14
|
+
virtual-plc --port 5000 --text-pad 0x20
|
|
15
|
+
virtual-plc --profile iQ-R --port 5001 --initial-state STOP
|
|
16
|
+
virtual-plc --init seed.json --fault-plan faults.json --dump-on-exit saved.json
|
|
17
|
+
```
|
|
18
|
+
<!-- /guide:run -->
|
|
19
|
+
|
|
20
|
+
The third recipe needs the JSON files shown below. From a checkout, install
|
|
21
|
+
with `python -m pip install -e .`. `python -m virtual_plc` is the equivalent
|
|
22
|
+
entry point. For more startup details, run `virtual-plc guide run`.
|
|
23
|
+
|
|
24
|
+
Install the client separately with `pip install pymcprotocol`:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from pymcprotocol import Type3E
|
|
28
|
+
|
|
29
|
+
client = Type3E(plctype="Q")
|
|
30
|
+
client.connect("127.0.0.1", 5000)
|
|
31
|
+
try:
|
|
32
|
+
client.batchwrite_wordunits("D0", [123, -45])
|
|
33
|
+
assert client.batchread_wordunits("D0", 2) == [123, -45]
|
|
34
|
+
finally:
|
|
35
|
+
client.close()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For the iQ-R recipe (`virtual-plc --profile iQ-R --port 5001 --initial-state STOP`):
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pymcprotocol import Type3E
|
|
42
|
+
|
|
43
|
+
client = Type3E(plctype="iQ-R")
|
|
44
|
+
try:
|
|
45
|
+
client.connect("127.0.0.1", 5001)
|
|
46
|
+
assert client.batchread_wordunits("D0", 1) == [0]
|
|
47
|
+
finally:
|
|
48
|
+
client.close()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use localhost only. Passwords simulate PLC locking; there is no network
|
|
52
|
+
authentication. Never connect to real equipment or production networks.
|
|
53
|
+
Non-local binds print a warning, not an access control.
|
|
54
|
+
|
|
55
|
+
## CLI reference
|
|
56
|
+
|
|
57
|
+
Run `virtual-plc guide options` for the option/default/meaning table generated
|
|
58
|
+
from the parser, or `virtual-plc --help`. For programs, use
|
|
59
|
+
`virtual-plc guide options --json`. Bad configuration exits 2 with a topic
|
|
60
|
+
hint; bind failures exit 1 without a traceback.
|
|
61
|
+
|
|
62
|
+
The stderr banner reports the bound address (including the assigned port for
|
|
63
|
+
`--port 0`), profile/state, idle timeout, text pad, dword order and fault plan.
|
|
64
|
+
Ctrl+C, SIGTERM and Windows SIGBREAK stop workers and save exit snapshots.
|
|
65
|
+
SIGBREAK needs a shared console; forced termination bypasses Python handlers.
|
|
66
|
+
`-v` logs request summaries and close reasons, excluding payloads/passwords.
|
|
67
|
+
|
|
68
|
+
| Guide topic | Contents |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `virtual-plc guide run` | Installation, startup and shutdown |
|
|
71
|
+
| `virtual-plc guide options` | All flags and defaults, generated from argparse |
|
|
72
|
+
| `virtual-plc guide client` | Client reads/writes, control and addressing |
|
|
73
|
+
| `virtual-plc guide profiles` | Generated command/frame/wire/profile/device matrix |
|
|
74
|
+
| `virtual-plc guide embed` | Python API and pytest fixture |
|
|
75
|
+
| `virtual-plc guide faults` | Plan schema, actions and three examples |
|
|
76
|
+
| `virtual-plc guide memory` | Seeding, snapshots, ranges and devices |
|
|
77
|
+
| `virtual-plc guide troubleshoot` | Symptoms and fixes |
|
|
78
|
+
| `virtual-plc guide divergences` | Simulator policies and unverified PLC behavior |
|
|
79
|
+
|
|
80
|
+
Every topic accepts `--json`. Unknown topics list the choices and exit 2.
|
|
81
|
+
|
|
82
|
+
## Profiles, frames and encodings
|
|
83
|
+
|
|
84
|
+
Commands support 3E/4E and binary/ASCII on all profiles. Q, L, QnA and iQ-L
|
|
85
|
+
share a wire class; iQ-R uses different descriptors/subcommands. Client
|
|
86
|
+
`plctype` and server `--profile` must agree for iQ-R. Inspect
|
|
87
|
+
`virtual-plc guide profiles --json` for the current matrix and device kinds.
|
|
88
|
+
|
|
89
|
+
This local recipe covers every profile/frame/wire combination:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from itertools import product
|
|
93
|
+
from pymcprotocol import Type3E, Type4E
|
|
94
|
+
from virtual_plc import Server
|
|
95
|
+
|
|
96
|
+
for profile in ("Q", "L", "QnA", "iQ-L", "iQ-R"):
|
|
97
|
+
with Server(profile=profile) as server:
|
|
98
|
+
for kind, wire in product((Type3E, Type4E), ("binary", "ascii")):
|
|
99
|
+
client = kind(plctype=profile)
|
|
100
|
+
client.setaccessopt(commtype=wire)
|
|
101
|
+
client.connect(*server.address)
|
|
102
|
+
try:
|
|
103
|
+
client.batchwrite_wordunits("D0", [123])
|
|
104
|
+
assert client.batchread_wordunits("D0", 1) == [123]
|
|
105
|
+
finally:
|
|
106
|
+
client.close()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Memory seeding, snapshots and embedding
|
|
110
|
+
|
|
111
|
+
Save seed.json (decimal address keys, unsigned native point values):
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{"D": {"100": [1, 2, 3]}, "M": {"0": [1, 0, 1]}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`--init` loads before listening; `--dump-on-exit` saves the same schema after
|
|
118
|
+
graceful shutdown. Dumps preserve explicit zeros and include SD203. Loads
|
|
119
|
+
validate the complete snapshot before overlaying data. See
|
|
120
|
+
`virtual-plc guide memory` for bounds, retention and device names.
|
|
121
|
+
|
|
122
|
+
Latch ranges are cleared by latchclear and preserved by remote_run(clear_mode=1).
|
|
123
|
+
SM and SD are never cleared; latchclear requires STOP.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from virtual_plc import Server, Memory, FaultPlan, __version__
|
|
127
|
+
|
|
128
|
+
memory = Memory()
|
|
129
|
+
memory.load({"D": {"100": [1, 2, 3]}})
|
|
130
|
+
with Server(memory=memory, fault_plan=FaultPlan([])) as server:
|
|
131
|
+
print(server.address, __version__)
|
|
132
|
+
# Connect clients here. Context exit stops and joins workers.
|
|
133
|
+
snapshot = memory.dump()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Seed before start/context entry; dump after stop. Direct Memory calls are not
|
|
137
|
+
synchronized with requests. Use clients for live writes. Server construction
|
|
138
|
+
initializes SD203; CLI seeding runs afterward. Writing SD203 changes the word
|
|
139
|
+
without changing CPU state. For a pytest fixture, run `virtual-plc guide embed`.
|
|
140
|
+
|
|
141
|
+
## Response faults
|
|
142
|
+
|
|
143
|
+
Save faults.json and pass `--fault-plan faults.json`:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{"steps": [{"match": {"command": "0x0619"}, "times": 1,
|
|
147
|
+
"action": {"type": "delay", "seconds": 0.02}}]}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Faults run after execution and do not roll back writes. Run
|
|
151
|
+
`virtual-plc guide faults` for the schema and examples; see
|
|
152
|
+
[FAULTS](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/FAULTS.md)
|
|
153
|
+
for delivery semantics.
|
|
154
|
+
|
|
155
|
+
## Troubleshooting
|
|
156
|
+
|
|
157
|
+
- Large reads: the client calls `recv(4096)` once. Split batches; the server's
|
|
158
|
+
point limit is separate. A raw reader can follow response lengths.
|
|
159
|
+
- CPU names: default Q returns `"VirtualPLC-Q\x00\x00\x00\x00"`. The client
|
|
160
|
+
retains NULs; `--text-pad 0x20` returns `"VirtualPLC-Q"`. Non-UTF-8 fill
|
|
161
|
+
such as 0xFF causes client decoding failure.
|
|
162
|
+
- Reset requires STOP. On receive/status failure, the client waits about 1 s
|
|
163
|
+
and reconnects. A normal reset keeps the connection open.
|
|
164
|
+
- Truncated or closed binary replies can look like status/value 0 because
|
|
165
|
+
the client does not validate headers/lengths. This is not proof of success.
|
|
166
|
+
- Idle timeout is disabled by default; explicit timeouts close idle or
|
|
167
|
+
incomplete connections. Reconnect afterward.
|
|
168
|
+
|
|
169
|
+
More fixes: `virtual-plc guide troubleshoot`. Protocol details:
|
|
170
|
+
[BEHAVIOR](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/BEHAVIOR.md).
|
|
171
|
+
|
|
172
|
+
## Known divergences from a real PLC
|
|
173
|
+
|
|
174
|
+
- CPU identities, ranges and SD203 state encoding are synthetic; SD203 mirrors
|
|
175
|
+
state in one direction. CPU names default to NUL fill on both encodings;
|
|
176
|
+
the client retains NULs. Physical fill remains unverified.
|
|
177
|
+
- ASCII addresses use decimal text even for hex-base devices. Dword order
|
|
178
|
+
defaults to low-word-first (`swapped`); `client` selects high-word-first.
|
|
179
|
+
Physical order is unverified; roundtrips remain symmetric.
|
|
180
|
+
- Locking is server-wide, survives disconnect and gates only CPU control.
|
|
181
|
+
It provides no authentication. Native iQ-R dword stride is one 32-bit point
|
|
182
|
+
per address; this and physical lock/reset policies are unverified.
|
|
183
|
+
- Timers/counters store values without execution. Monitoring timers are
|
|
184
|
+
parsed/ignored; routes alias one memory/state. Error meanings await checks.
|
|
185
|
+
|
|
186
|
+
Run `virtual-plc guide divergences` for the compact list of simulator policies.
|
|
187
|
+
|
|
188
|
+
## Development and releases
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
python -m pip install -e . -r requirements-dev.txt
|
|
192
|
+
python -m pytest -q
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The dev extra installs pytest only. The test extra adds the compatible oracle
|
|
196
|
+
range declared in pyproject.toml. requirements-dev.txt is the only exact client
|
|
197
|
+
pin. Dependabot updates that file and pyproject; the coverage guard rejects
|
|
198
|
+
newly uncovered public methods, device constants and lookup-only names.
|
|
199
|
+
|
|
200
|
+
The [wheel hash](https://github.com/greatgc-flow/VirtualPLC/blob/main/tests/oracle_pin.txt)
|
|
201
|
+
is checked when a matching-version wheel is retained under .venv/oracle. A
|
|
202
|
+
bumped oracle skips only the stale artifact-hash check; the full suite and
|
|
203
|
+
coverage guard still run. Oracle watch installs the latest client and changes
|
|
204
|
+
only that run's local version expectation, without committing the change.
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
python -m pip install build twine
|
|
208
|
+
python -m build
|
|
209
|
+
python -m twine check dist/*
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
CI tests Windows/Linux on Python 3.10–3.13 and installs built wheels in clean
|
|
213
|
+
environments. Tags must match pyproject version; trusted publishing requires
|
|
214
|
+
the PyPI publisher and GitHub environment pypi to be configured.
|
|
215
|
+
|
|
216
|
+
See [DESIGN](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/DESIGN.md),
|
|
217
|
+
[BEHAVIOR](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/BEHAVIOR.md)
|
|
218
|
+
and [FAULTS](https://github.com/greatgc-flow/VirtualPLC/blob/main/docs/FAULTS.md).
|
|
219
|
+
Licensed under [MIT](https://github.com/greatgc-flow/VirtualPLC/blob/main/LICENSE).
|