@aryacode/dsh-engineering 0.0.0-stage → 0.3.0
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.
- package/CHANGELOG.md +115 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/cordis.patch.yml +122 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +77 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""EtherNet/IP assembly map: how much of each assembly a project actually uses.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS
|
|
4
|
+
---------------
|
|
5
|
+
"Is there room for another status word?" is asked every time a diagnostic is added, and answering
|
|
6
|
+
it took several steps in the session that prompted this: the ``%QW`` base, the
|
|
7
|
+
``I.Data[n] = %QW(base + 2n)`` rule, a scan of the project's declarations, and the module's
|
|
8
|
+
``PrimCxnInputSize``/``PrimCxnOutputSize`` attributes in the L5X. Every one of those is a fact
|
|
9
|
+
that can be read, so the answer belongs in one call rather than in a reconstruction.
|
|
10
|
+
|
|
11
|
+
WHAT IT READS, AND FROM WHERE
|
|
12
|
+
-----------------------------
|
|
13
|
+
Two independent sources, reported separately because they can disagree:
|
|
14
|
+
|
|
15
|
+
* the **L5X** module definition gives the assembly SIZES, in bytes, as the scanner declares them
|
|
16
|
+
(``Communications/@PrimCxnInputSize`` and ``PrimCxnOutputSize``);
|
|
17
|
+
* the **MotionWorks project** gives the USED range, because every mapped word and bit is a
|
|
18
|
+
declared variable carrying an IEC address such as ``%QX21488.0`` or ``%QW21492``.
|
|
19
|
+
|
|
20
|
+
Neither alone answers the question. Sizes without usage cannot say how much is left; usage
|
|
21
|
+
without sizes cannot say what is left *of*.
|
|
22
|
+
|
|
23
|
+
ADDRESS ARITHMETIC
|
|
24
|
+
------------------
|
|
25
|
+
The addresses are 16-bit word addresses, and ``.n`` names a bit. Measured on the TopCutter
|
|
26
|
+
interface: ``%IX21488.0`` is ``O.Data[0].0`` and ``%IW21490`` is ``O.Data[1]``, so a data word is
|
|
27
|
+
two address units. The byte width of a word is therefore what turns bytes into words:
|
|
28
|
+
``words = bytes / 2``. The module and the project both use 16-bit ``INT`` elements.
|
|
29
|
+
|
|
30
|
+
THE ONE THING THIS DOES NOT DO
|
|
31
|
+
------------------------------
|
|
32
|
+
It does not decide that a gap is usable. It reports the first word address that is NOT used and
|
|
33
|
+
the room remaining after it, which is arithmetic over what was read; whether an unused word is
|
|
34
|
+
free to take is a question about the peer's program, and guessing at that is how an interface
|
|
35
|
+
ends up with two things writing the same offset.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import re
|
|
41
|
+
import xml.etree.ElementTree as ET
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
#: ``%QW21492``, ``%IX21488.0``, ``%IB21488`` — the IEC address forms this understands.
|
|
45
|
+
_ADDRESS = re.compile(
|
|
46
|
+
r"^%(?P<area>[IQM])(?P<size>[XBWDL])(?P<word>\d+)(?:\.(?P<bit>\d+))?$",
|
|
47
|
+
re.IGNORECASE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
#: Bits per word, and the byte width of one address unit, for a data word on this interface.
|
|
51
|
+
#: Measured, not assumed: O.Data[n] advances the address by 2, and each element is an INT.
|
|
52
|
+
ADDRESS_UNITS_PER_WORD = 2
|
|
53
|
+
BYTES_PER_WORD = 2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def read_project_addresses(project_root: Path | str) -> list[dict]:
|
|
57
|
+
"""Every declared variable in the project that carries an IEC address.
|
|
58
|
+
|
|
59
|
+
Uses the same declaration reader the rest of the engine uses, so this cannot drift from what
|
|
60
|
+
mw_code_globals and mw_code_read_st report.
|
|
61
|
+
"""
|
|
62
|
+
from . import project as P
|
|
63
|
+
|
|
64
|
+
root = Path(project_root)
|
|
65
|
+
proj = P.Project(root=root)
|
|
66
|
+
found: list[dict] = []
|
|
67
|
+
|
|
68
|
+
def collect(table, owner: str) -> None:
|
|
69
|
+
for v in getattr(table, "variables", []) or []:
|
|
70
|
+
address = getattr(v, "address", None)
|
|
71
|
+
if not address:
|
|
72
|
+
continue
|
|
73
|
+
found.append({
|
|
74
|
+
"name": getattr(v, "name", None),
|
|
75
|
+
"type": getattr(v, "type_name", None),
|
|
76
|
+
"address": str(address),
|
|
77
|
+
"owner": owner,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
collect(proj.global_variables(), "(globals)")
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
for info in proj.pous():
|
|
85
|
+
try:
|
|
86
|
+
collect(info.declarations(), info.name)
|
|
87
|
+
except Exception:
|
|
88
|
+
continue
|
|
89
|
+
return found
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _data_words(addresses: list[dict]) -> dict:
|
|
93
|
+
"""Group ``%I``/``%Q`` addresses into the used word set, per direction.
|
|
94
|
+
|
|
95
|
+
``.`` marks a bit inside a word, so ``%IX21488.0`` and ``%IW21490`` both occupy a word: the
|
|
96
|
+
first shares its word with its neighbours, the second owns one. Both are recorded as used,
|
|
97
|
+
which is what "is there room" has to be computed against.
|
|
98
|
+
"""
|
|
99
|
+
used: dict[str, set[int]] = {"input": set(), "output": set()}
|
|
100
|
+
detail: dict[str, list[dict]] = {"input": [], "output": []}
|
|
101
|
+
unparsed: list[str] = []
|
|
102
|
+
|
|
103
|
+
for entry in addresses:
|
|
104
|
+
m = _ADDRESS.match(entry["address"])
|
|
105
|
+
if not m:
|
|
106
|
+
unparsed.append(entry["address"])
|
|
107
|
+
continue
|
|
108
|
+
area = m.group("area").upper()
|
|
109
|
+
if area not in ("I", "Q"):
|
|
110
|
+
continue # %M (internal marker) is not part of an assembly
|
|
111
|
+
direction = "input" if area == "I" else "output"
|
|
112
|
+
word = int(m.group("word"))
|
|
113
|
+
size = m.group("size").upper()
|
|
114
|
+
bit = m.group("bit")
|
|
115
|
+
if size == "X" and bit is not None:
|
|
116
|
+
# A bit address names its word, and the word it names is the one in use.
|
|
117
|
+
used[direction].add(word)
|
|
118
|
+
detail[direction].append({**entry, "direction": direction, "word": word, "kind": "bit"})
|
|
119
|
+
elif size in ("W", "B", "D", "L"):
|
|
120
|
+
# A whole-element address. W is one word; the wider forms occupy more than one, and
|
|
121
|
+
# this reports the span rather than pretending they are all one word wide.
|
|
122
|
+
span = {"W": 1, "B": 1, "D": 2, "L": 2}[size]
|
|
123
|
+
# Address units: a word address advances by ADDRESS_UNITS_PER_WORD, so each
|
|
124
|
+
# extra data word advances the address by that much.
|
|
125
|
+
for i in range(span):
|
|
126
|
+
used[direction].add(word + i * ADDRESS_UNITS_PER_WORD)
|
|
127
|
+
detail[direction].append({
|
|
128
|
+
**entry, "direction": direction, "word": word, "kind": f"element({size})",
|
|
129
|
+
"words": span,
|
|
130
|
+
})
|
|
131
|
+
else:
|
|
132
|
+
unparsed.append(entry["address"])
|
|
133
|
+
|
|
134
|
+
return {"used": used, "detail": detail, "unparsed": unparsed}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _runs(words: set[int]) -> list[dict]:
|
|
138
|
+
"""Contiguous runs of used word addresses, so a gap is visible as a gap."""
|
|
139
|
+
if not words:
|
|
140
|
+
return []
|
|
141
|
+
ordered = sorted(words)
|
|
142
|
+
runs = []
|
|
143
|
+
start = prev = ordered[0]
|
|
144
|
+
for w in ordered[1:]:
|
|
145
|
+
if w == prev + ADDRESS_UNITS_PER_WORD:
|
|
146
|
+
prev = w
|
|
147
|
+
continue
|
|
148
|
+
runs.append({"first": start, "last": prev, "words": (prev - start) // ADDRESS_UNITS_PER_WORD + 1})
|
|
149
|
+
start = prev = w
|
|
150
|
+
runs.append({"first": start, "last": prev, "words": (prev - start) // ADDRESS_UNITS_PER_WORD + 1})
|
|
151
|
+
return runs
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def read_l5x_module(l5x: Path | str, module_name: str | None = None) -> dict:
|
|
155
|
+
"""The Ethernet module's declared assembly sizes, from an exported L5X.
|
|
156
|
+
|
|
157
|
+
Reads the ``<Module>`` whose ``CatalogNumber`` is ``ETHERNET-MODULE`` — a generic
|
|
158
|
+
EtherNet/IP module — because that is the shape this interface uses. A named module wins when
|
|
159
|
+
the caller passes one.
|
|
160
|
+
"""
|
|
161
|
+
path = Path(l5x)
|
|
162
|
+
if not path.is_file():
|
|
163
|
+
raise FileNotFoundError(f"L5X not found: {path}")
|
|
164
|
+
tree = ET.parse(path)
|
|
165
|
+
root = tree.getroot()
|
|
166
|
+
|
|
167
|
+
candidates = []
|
|
168
|
+
for module in root.iter("Module"):
|
|
169
|
+
if module_name and module.get("Name") != module_name:
|
|
170
|
+
continue
|
|
171
|
+
if not module_name and module.get("CatalogNumber") != "ETHERNET-MODULE":
|
|
172
|
+
continue
|
|
173
|
+
comms = module.find("Communications")
|
|
174
|
+
if comms is None:
|
|
175
|
+
continue
|
|
176
|
+
candidates.append({
|
|
177
|
+
"name": module.get("Name"),
|
|
178
|
+
"catalog": module.get("CatalogNumber"),
|
|
179
|
+
"vendor": module.get("Vendor"),
|
|
180
|
+
"product_type": module.get("ProductType"),
|
|
181
|
+
"product_code": module.get("ProductCode"),
|
|
182
|
+
"revision": f"{module.get('Major')}.{module.get('Minor')}",
|
|
183
|
+
"input_bytes": int(comms.get("PrimCxnInputSize") or 0),
|
|
184
|
+
"output_bytes": int(comms.get("PrimCxnOutputSize") or 0),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
if not candidates:
|
|
188
|
+
named = f" named '{module_name}'" if module_name else ""
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"no EtherNet/IP module{named} with a <Communications> element in {path.name}. "
|
|
191
|
+
"Pass module_name if the module is not a generic ETHERNET-MODULE."
|
|
192
|
+
)
|
|
193
|
+
return {"l5x": str(path), "modules": candidates}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_map(
|
|
197
|
+
project_root: Path | str | None = None,
|
|
198
|
+
l5x: Path | str | None = None,
|
|
199
|
+
module_name: str | None = None,
|
|
200
|
+
) -> dict:
|
|
201
|
+
"""Assemble the answer from whichever sources the caller supplied."""
|
|
202
|
+
report: dict = {"automatic_changes": False}
|
|
203
|
+
|
|
204
|
+
modules: list[dict] = []
|
|
205
|
+
if l5x:
|
|
206
|
+
report["module_source"] = read_l5x_module(l5x, module_name)
|
|
207
|
+
modules = report["module_source"]["modules"]
|
|
208
|
+
|
|
209
|
+
used = {"input": set(), "output": set()}
|
|
210
|
+
detail = {"input": [], "output": []}
|
|
211
|
+
unparsed: list[str] = []
|
|
212
|
+
if project_root:
|
|
213
|
+
parsed = _data_words(read_project_addresses(project_root))
|
|
214
|
+
used, detail, unparsed = parsed["used"], parsed["detail"], parsed["unparsed"]
|
|
215
|
+
|
|
216
|
+
assemblies: list[dict] = []
|
|
217
|
+
for direction, size_key in (("input", "input_bytes"), ("output", "output_bytes")):
|
|
218
|
+
entry: dict = {"direction": direction}
|
|
219
|
+
words = used[direction]
|
|
220
|
+
declared_words = None
|
|
221
|
+
if modules:
|
|
222
|
+
declared_words = modules[0][size_key] // BYTES_PER_WORD
|
|
223
|
+
entry["declared_bytes"] = modules[0][size_key]
|
|
224
|
+
entry["declared_words"] = declared_words
|
|
225
|
+
|
|
226
|
+
# THE ASSEMBLY IS A WINDOW, NOT EVERY ADDRESS IN THE PROJECT.
|
|
227
|
+
#
|
|
228
|
+
# Measured: the project carries %I/%Q addresses at 21488 AND at 61443, and a naive
|
|
229
|
+
# min/max over both reported a 64-word assembly spanning 39,955 words with the "next free"
|
|
230
|
+
# address at 61445. Both numbers were arithmetically faithful and completely wrong: they
|
|
231
|
+
# described two separate I/O areas as one.
|
|
232
|
+
#
|
|
233
|
+
# The base is the lowest address in the direction, which is what a mapped assembly starts
|
|
234
|
+
# at; anything at or beyond base + declared span is a different module's area and is
|
|
235
|
+
# reported separately rather than folded in.
|
|
236
|
+
outside: list[int] = []
|
|
237
|
+
if declared_words is not None and words:
|
|
238
|
+
base = min(words)
|
|
239
|
+
limit = base + declared_words * ADDRESS_UNITS_PER_WORD
|
|
240
|
+
inside = {w for w in words if base <= w < limit}
|
|
241
|
+
outside = sorted(w for w in words if w >= limit)
|
|
242
|
+
entry["base_word_address"] = base
|
|
243
|
+
entry["window_last_word_address"] = limit - ADDRESS_UNITS_PER_WORD
|
|
244
|
+
words = inside
|
|
245
|
+
|
|
246
|
+
entry["used_words"] = len(words)
|
|
247
|
+
entry["used_runs"] = _runs(words)
|
|
248
|
+
if words:
|
|
249
|
+
entry["first_word_address"] = min(words)
|
|
250
|
+
entry["last_word_address"] = max(words)
|
|
251
|
+
entry["span_words"] = (max(words) - min(words)) // ADDRESS_UNITS_PER_WORD + 1
|
|
252
|
+
if declared_words is not None:
|
|
253
|
+
entry["unused_words"] = declared_words - entry["used_words"]
|
|
254
|
+
entry["full"] = entry["unused_words"] <= 0
|
|
255
|
+
# THE ANSWER TO THE QUESTION, when it can be given: the first word address past the
|
|
256
|
+
# highest one in use. Whether the PEER leaves it alone is not knowable from here.
|
|
257
|
+
if words:
|
|
258
|
+
entry["next_free_word_address"] = max(words) + ADDRESS_UNITS_PER_WORD
|
|
259
|
+
entry["contiguous_room_after_last"] = (
|
|
260
|
+
entry["window_last_word_address"] - max(words)
|
|
261
|
+
) // ADDRESS_UNITS_PER_WORD
|
|
262
|
+
if outside:
|
|
263
|
+
entry["addresses_outside_this_assembly"] = {
|
|
264
|
+
"count": len(outside),
|
|
265
|
+
"first": outside[0],
|
|
266
|
+
"note": (
|
|
267
|
+
"these %I/%Q addresses lie at or beyond this assembly's declared span, so they "
|
|
268
|
+
"belong to a different module's area; they are NOT part of this map and this "
|
|
269
|
+
"map's unused_words does not describe them"
|
|
270
|
+
),
|
|
271
|
+
}
|
|
272
|
+
entry["elements"] = detail[direction]
|
|
273
|
+
assemblies.append(entry)
|
|
274
|
+
|
|
275
|
+
report["assemblies"] = assemblies
|
|
276
|
+
report["address_rules"] = {
|
|
277
|
+
"unit": "16-bit word",
|
|
278
|
+
"bytes_per_word": BYTES_PER_WORD,
|
|
279
|
+
"address_units_per_word": ADDRESS_UNITS_PER_WORD,
|
|
280
|
+
"note": (
|
|
281
|
+
"measured on this interface: O.Data[1] is %IW21490 and O.Data[0].0 is %IX21488.0, so "
|
|
282
|
+
"each data word advances the address by 2 and each element is one 16-bit INT"
|
|
283
|
+
),
|
|
284
|
+
}
|
|
285
|
+
if unparsed:
|
|
286
|
+
report["addresses_not_understood"] = sorted(set(unparsed))
|
|
287
|
+
report["note"] = (
|
|
288
|
+
"used_words counts declared variables carrying an %I/%Q address; it does not prove the "
|
|
289
|
+
"peer program leaves the remaining words alone, so confirm a new offset against the "
|
|
290
|
+
"CompactLogix side before writing it"
|
|
291
|
+
)
|
|
292
|
+
return report
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Typed errors that surface as actionable messages in MCP tool results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MotionWorksError(Exception):
|
|
7
|
+
"""Base class for every failure this package reports to an agent."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectNotFound(MotionWorksError):
|
|
11
|
+
"""The requested project could not be resolved to an expanded directory."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AmbiguousProject(MotionWorksError):
|
|
15
|
+
"""A bare project name matched more than one project on disk."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IdeRunning(MotionWorksError):
|
|
19
|
+
"""MotionWorks IEC is open, so writing would be unsafe.
|
|
20
|
+
|
|
21
|
+
The IDE caches project state in memory and rewrites whole files on save, so
|
|
22
|
+
an external edit made while it is open will either be silently discarded or
|
|
23
|
+
produce an inconsistent project.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnsupportedFormat(MotionWorksError):
|
|
28
|
+
"""The on-disk format differs from the versions this package understands."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VerificationFailed(MotionWorksError):
|
|
32
|
+
"""A post-write read-back did not match what was written."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NotFound(MotionWorksError):
|
|
36
|
+
"""A named POU, variable, or file does not exist in the project."""
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Read bounded compiler networks with exact POU and worksheet identities.
|
|
2
|
+
|
|
3
|
+
No binary graphical decoding or writes. The caller must bind this cache to a
|
|
4
|
+
fresh native Build and unchanged complete source baseline.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path, PureWindowsPath
|
|
9
|
+
|
|
10
|
+
HEADER = re.compile(r'\A\(\*\s*T: (PROGRAM|FUNCTION_BLOCK|FUNCTION) ([A-Za-z_][A-Za-z_0-9]*)\s', re.S)
|
|
11
|
+
SYMBOL = re.compile(r'@IFBP (\d+)\.(\d+)(?![\d.])|@(RV|IV|IFB) (\d+)(?!\d)')
|
|
12
|
+
DECLARATION = re.compile(r'^([A-Za-z_][A-Za-z_0-9]*)\t(\d+)\t(VAR[A-Z_]*)\t([^\r\n]+)\r?\n([^;]*);', re.M)
|
|
13
|
+
# Generated private code temporaries are counted for completeness, never exposed
|
|
14
|
+
# as public pins. CamGenerator contains @T_Code_00 after its 140 source rows.
|
|
15
|
+
DEPENDENCY_DECLARATION = re.compile(r'^((?:[A-Za-z_][A-Za-z_0-9@]*|@T_Code_[0-9]+))\t(\d+)\t(VAR[A-Z_]*)\t([^\r\n]+)\r?\n([^;]*);', re.M)
|
|
16
|
+
MAX_BYTES = 2_000_000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_text(path, root):
|
|
20
|
+
if not path.resolve().is_relative_to(root.resolve()):
|
|
21
|
+
raise ValueError('Compiler artifact resolves outside project')
|
|
22
|
+
before = path.stat()
|
|
23
|
+
raw = path.read_bytes()
|
|
24
|
+
after = path.stat()
|
|
25
|
+
if (before.st_size,before.st_mtime_ns)!=(after.st_size,after.st_mtime_ns):
|
|
26
|
+
raise ValueError('Compiler artifact changed during read')
|
|
27
|
+
if len(raw) > MAX_BYTES or b'\0' in raw:
|
|
28
|
+
raise ValueError('Unsupported compiler text size/encoding')
|
|
29
|
+
return raw.decode('latin1'), dict(path=str(path), sha256=hashlib.sha256(raw).hexdigest(), bytes=len(raw), modified_ms=after.st_mtime_ns/1_000_000)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def identity(text):
|
|
33
|
+
match = HEADER.match(text)
|
|
34
|
+
if not match:
|
|
35
|
+
raise ValueError('Unknown compiler listing header')
|
|
36
|
+
return match.groups()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def symbols(text, saved=None):
|
|
40
|
+
result, rows, ordinals, names = {}, [], set(), set()
|
|
41
|
+
def bind(token, row):
|
|
42
|
+
if token in result and result[token] != row:
|
|
43
|
+
raise ValueError('Ambiguous compiler symbol')
|
|
44
|
+
result[token] = row
|
|
45
|
+
for match in DECLARATION.finditer(text):
|
|
46
|
+
name, ordinal, section, type_token, detail = match.groups()
|
|
47
|
+
if ordinal in ordinals or name.casefold() in names or int(ordinal) < 1:
|
|
48
|
+
raise ValueError('Ambiguous compiler declaration ordinal/name')
|
|
49
|
+
ordinals.add(ordinal); names.add(name.casefold())
|
|
50
|
+
row = dict(name=name, section=section)
|
|
51
|
+
rows.append((name, section))
|
|
52
|
+
explicit = re.findall(r'^@(RV|IV) (\d+)(?:\s|$)', detail, re.M)
|
|
53
|
+
if len(explicit) > 1:
|
|
54
|
+
raise ValueError('Ambiguous compiler declaration token')
|
|
55
|
+
if explicit:
|
|
56
|
+
kind, number = explicit[0]
|
|
57
|
+
bind('@' + kind + ' ' + number, row)
|
|
58
|
+
elif section in ('VAR', 'VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'VAR_TEMP'):
|
|
59
|
+
# Local operands use the declaration ordinal. FB instances use the
|
|
60
|
+
# same ordinal under IFB; the type ID remains raw compiler evidence.
|
|
61
|
+
if re.fullmatch(r'@FB:\d+\s*', type_token):
|
|
62
|
+
bind('@IFB ' + ordinal, row)
|
|
63
|
+
elif re.match(r'@TYP:\d+(?:\s|$)', type_token):
|
|
64
|
+
bind('@IV ' + ordinal, row)
|
|
65
|
+
if saved is not None:
|
|
66
|
+
if sorted(rows) != sorted((v.name, v.section) for v in saved):
|
|
67
|
+
raise ValueError('Compiler declarations differ from saved worksheet')
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def pin_bindings(code, declarations, saved, path, root):
|
|
72
|
+
"""Bind only public pins from exact compiler FB dependencies, never UI order."""
|
|
73
|
+
needed = {m.group(1) for m in SYMBOL.finditer(code) if m.group(1) is not None}
|
|
74
|
+
saved_types = {v.name: v.type_name for v in saved}
|
|
75
|
+
pins, artifacts, cache = {}, [], {}
|
|
76
|
+
directions = {'VAR_INPUT': 'input', 'VAR_OUTPUT': 'output', 'VAR_IN_OUT': 'in_out'}
|
|
77
|
+
for match in DECLARATION.finditer(declarations):
|
|
78
|
+
name, ordinal, section, type_token, _ = match.groups()
|
|
79
|
+
fb = re.fullmatch(r'@FB:(\d+)\s*', type_token)
|
|
80
|
+
if ordinal not in needed or not fb or section not in ('VAR', 'VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'VAR_TEMP'):
|
|
81
|
+
continue
|
|
82
|
+
type_id = int(fb.group(1))
|
|
83
|
+
if type_id not in cache:
|
|
84
|
+
dependency = path.with_name(f'ICI{type_id:05d}.DIT')
|
|
85
|
+
text, evidence = read_text(dependency, root)
|
|
86
|
+
kind, block = identity(text)
|
|
87
|
+
if '*)' not in text:
|
|
88
|
+
raise ValueError('Truncated compiler FB dependency header')
|
|
89
|
+
header = text.split('*)', 1)[0]
|
|
90
|
+
ids = re.findall(r'^CI#:\s*(\d+)\s*$', header, re.M)
|
|
91
|
+
counts = re.findall(r'^QVE:\s*(\d+)\s*$', header, re.M)
|
|
92
|
+
if kind != 'FUNCTION_BLOCK' or len(ids) != 1 or int(ids[0]) != type_id:
|
|
93
|
+
raise ValueError('Compiler FB dependency identity mismatch')
|
|
94
|
+
# Graphical library FBs contain compiler-only names such as Code@@80.
|
|
95
|
+
# Count those private rows, but never expose them as public pins.
|
|
96
|
+
rows = list(DEPENDENCY_DECLARATION.finditer(text))
|
|
97
|
+
# QVE includes internal declarations; a partial parse must not resolve pins.
|
|
98
|
+
if len(counts) != 1 or int(counts[0]) != len(rows):
|
|
99
|
+
raise ValueError('Compiler FB dependency declarations are incomplete')
|
|
100
|
+
symbols(text) # Reject duplicate names/ordinals and ambiguous bindings.
|
|
101
|
+
names = {r.group(1).casefold() for r in rows}
|
|
102
|
+
ordinals = {int(r.group(2)) for r in rows}
|
|
103
|
+
if len(names) != len(rows) or ordinals != set(range(1, len(rows)+1)):
|
|
104
|
+
raise ValueError('Compiler FB dependency ordinals are incomplete')
|
|
105
|
+
cache[type_id] = (block, rows, evidence)
|
|
106
|
+
artifacts.append(evidence)
|
|
107
|
+
block, rows, evidence = cache[type_id]
|
|
108
|
+
if saved_types[name].casefold() != block.casefold():
|
|
109
|
+
raise ValueError('Compiler FB dependency type differs from saved instance')
|
|
110
|
+
for row in rows:
|
|
111
|
+
pin_name, pin_ordinal, pin_section, _, _ = row.groups()
|
|
112
|
+
if pin_section in directions:
|
|
113
|
+
if not re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', pin_name):
|
|
114
|
+
raise ValueError('Compiler FB public pin name is unsupported')
|
|
115
|
+
pins[f'@IFBP {ordinal}.{int(pin_ordinal)}'] = dict(
|
|
116
|
+
declaration=dict(name=pin_name, section=pin_section),
|
|
117
|
+
pin_direction=directions[pin_section], block_type=block,
|
|
118
|
+
compiler_type_id=type_id, dependency_sha256=evidence['sha256'])
|
|
119
|
+
return pins, artifacts
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def networks(text, bindings, pins=None):
|
|
123
|
+
result, current = [], None
|
|
124
|
+
if '*)' not in text:
|
|
125
|
+
raise ValueError('Truncated compiler header')
|
|
126
|
+
for line in text.split('*)', 1)[1].splitlines():
|
|
127
|
+
if line.strip() == '@NETWORK_BEGIN':
|
|
128
|
+
if current is not None:
|
|
129
|
+
raise ValueError('Nested compiler network')
|
|
130
|
+
current = []
|
|
131
|
+
elif line.strip() == '@NETWORK_END':
|
|
132
|
+
if current is None:
|
|
133
|
+
raise ValueError('Unmatched compiler network end')
|
|
134
|
+
result.append(dict(number=len(result)+1, lines=current))
|
|
135
|
+
current = None
|
|
136
|
+
elif line.strip():
|
|
137
|
+
if current is None:
|
|
138
|
+
raise ValueError('Instruction outside graphical network')
|
|
139
|
+
refs = []
|
|
140
|
+
for match in SYMBOL.finditer(line):
|
|
141
|
+
token = match.group()
|
|
142
|
+
if match.group(1) is not None:
|
|
143
|
+
instance = bindings.get('@IFB ' + match.group(1))
|
|
144
|
+
pin = (pins or {}).get(token) if instance else None
|
|
145
|
+
refs.append(dict(dict(token=token, declaration=None, resolved=pin is not None,
|
|
146
|
+
instance=instance, pin_ordinal=int(match.group(2))), **(pin or {})))
|
|
147
|
+
else:
|
|
148
|
+
refs.append(dict(token=token, declaration=bindings.get(token), resolved=token in bindings))
|
|
149
|
+
current.append(dict(raw=line, symbols=refs))
|
|
150
|
+
if current is not None:
|
|
151
|
+
raise ValueError('Truncated compiler network')
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def inspect(project, name, start=1, limit=10, *, source_only=False):
|
|
156
|
+
if type(start) is not int or start < 1 or type(limit) is not int or not 1 <= limit <= 50:
|
|
157
|
+
raise ValueError('Invalid network range')
|
|
158
|
+
pou = project.pou(name)
|
|
159
|
+
if pou.name != name or pou.body_stream()[1] not in (('ST', 'LD', 'FBD') if source_only else ('LD', 'FBD')):
|
|
160
|
+
raise ValueError('Exact graphical POU required')
|
|
161
|
+
root = project.root
|
|
162
|
+
candidates = []
|
|
163
|
+
for path in sorted((root / 'C').glob('*/R/*/ICI*.CIC')):
|
|
164
|
+
text, evidence = read_text(path, root)
|
|
165
|
+
if identity(text)[1] == name:
|
|
166
|
+
candidates.append((path, text, evidence))
|
|
167
|
+
if len(candidates) != 1:
|
|
168
|
+
raise ValueError('Compiler listing for exact POU absent or ambiguous; unused POUs may be omitted. Inspect usage without automatic task assignment')
|
|
169
|
+
path, code, evidence = candidates[0]
|
|
170
|
+
declarations, dit_evidence = read_text(path.with_suffix('.DIT'), root)
|
|
171
|
+
if identity(declarations) != identity(code):
|
|
172
|
+
raise ValueError('Compiler declaration identity mismatch')
|
|
173
|
+
worksheet, diw_evidence = read_text(path.with_suffix('.DIW'), root)
|
|
174
|
+
# Match the saved body worksheet, not a guessed filename or a sibling cache.
|
|
175
|
+
expected = 'poe/' + name.casefold() + '/' + pou.body_stream()[0].casefold()
|
|
176
|
+
rows = [line.split('\t') for line in worksheet.splitlines() if re.match(r'^\d+\t\d+\t', line)]
|
|
177
|
+
if len([r for r in rows if len(r)==3 and r[2].replace('\\','/').casefold()==expected]) != 1:
|
|
178
|
+
raise ValueError('Compiler worksheet identity mismatch')
|
|
179
|
+
mapping, sp_evidence = read_text(path.with_suffix('.SP'), root)
|
|
180
|
+
paths = [line.split('\t')[0] for line in mapping.splitlines() if line.strip() and not line.startswith('*\t')]
|
|
181
|
+
expected_file = str(root / Path(expected))
|
|
182
|
+
# Compiler Windows paths are compared as paths even in portable unit tests.
|
|
183
|
+
if paths and any(PureWindowsPath(p) != PureWindowsPath(expected_file) for p in paths):
|
|
184
|
+
raise ValueError('Compiler source mapping points to another project/worksheet')
|
|
185
|
+
table = pou.declarations()
|
|
186
|
+
if table.warnings:
|
|
187
|
+
raise ValueError('Saved declaration worksheet is incomplete')
|
|
188
|
+
bindings = symbols(declarations, table.variables)
|
|
189
|
+
pins, dependency_artifacts = ({}, []) if source_only else pin_bindings(code, declarations, table.variables, path, root)
|
|
190
|
+
parsed = [] if source_only else networks(code, bindings, pins)
|
|
191
|
+
if source_only and not paths:
|
|
192
|
+
raise ValueError('Compiled source has no worksheet mapping')
|
|
193
|
+
if parsed and not paths:
|
|
194
|
+
raise ValueError('Populated listing has no source mapping')
|
|
195
|
+
selected = parsed[start-1:start-1+limit]
|
|
196
|
+
artifacts=[evidence,dit_evidence,diw_evidence,sp_evidence]
|
|
197
|
+
for artifact in artifacts + dependency_artifacts:
|
|
198
|
+
if read_text(Path(artifact['path']),root)[1]!=artifact:
|
|
199
|
+
raise ValueError('Compiler artifact set changed during inspection')
|
|
200
|
+
if source_only:
|
|
201
|
+
return dict(pou=name, language=pou.body_stream()[1], artifacts=artifacts,
|
|
202
|
+
action_performed=False, compiler_cache_freshness_verified=False)
|
|
203
|
+
return dict(pou=name, language=pou.body_stream()[1], artifacts=artifacts,
|
|
204
|
+
network_count=len(parsed), start=start, networks=selected, has_more=start-1+len(selected)<len(parsed),
|
|
205
|
+
symbol_bindings=bindings, dependency_artifacts=dependency_artifacts,
|
|
206
|
+
compiler_cache_freshness_verified=False, compiler_dependency_freshness_verified=False,
|
|
207
|
+
action_performed=False, limitations=['Compiler tokens are retained verbatim; unresolved tokens have no inferred meaning.',
|
|
208
|
+
'Compiler instructions do not prove canvas geometry, pin placement or runtime behavior.'])
|