@aryacode/dsh-engineering 0.0.0-stage → 0.4.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 +128 -0
- package/LICENSE +21 -0
- package/README.md +230 -2
- package/cordis.patch.yml +123 -0
- package/lib/client.js +3448 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -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,472 @@
|
|
|
1
|
+
"""Parse MotionWorks IEC variable declarations.
|
|
2
|
+
|
|
3
|
+
A MotionWorks POU stores its variables in two places inside ``src.st1``:
|
|
4
|
+
|
|
5
|
+
``<POU>V.VB``
|
|
6
|
+
The authoritative *textual* IEC 61131-3 declarations. This is what the
|
|
7
|
+
MotionWorks variable grid is showing you, and it carries the names, types,
|
|
8
|
+
addresses, and ``(*...*)`` descriptions. It is plain text and it is
|
|
9
|
+
complete, so it is the primary source of truth here.
|
|
10
|
+
|
|
11
|
+
``<POU>V.VGR``
|
|
12
|
+
A binary "variable grid" record table holding the grid's UI state (handles,
|
|
13
|
+
row positions, usage flags, group assignment). It duplicates some of what
|
|
14
|
+
``.VB`` holds and adds editor metadata.
|
|
15
|
+
|
|
16
|
+
Format of ``.VB`` (observed across all 34 declaration streams in the sample
|
|
17
|
+
projects)::
|
|
18
|
+
|
|
19
|
+
<blank>
|
|
20
|
+
(*Group:Default*)
|
|
21
|
+
<blank>
|
|
22
|
+
<blank>
|
|
23
|
+
VAR_EXTERNAL
|
|
24
|
+
\tTopCutter :\tAXIS_REF;(*SGD7S - 1 (Do Not Modify!!) *)<CRLF>
|
|
25
|
+
\tEIP_FromCLX_Axis_SVON_Cmd :\tBOOL;<CRLF>
|
|
26
|
+
END_VAR
|
|
27
|
+
<blank>
|
|
28
|
+
<blank>
|
|
29
|
+
VAR
|
|
30
|
+
\tMC_TopCutter_ServoOn :\tMC_Power;<CRLF>
|
|
31
|
+
END_VAR
|
|
32
|
+
|
|
33
|
+
Only ``VAR`` and ``VAR_EXTERNAL`` appear in POU files; the resource-level
|
|
34
|
+
``Global_Variables.VB`` uses ``VAR_GLOBAL``. Blocks with the same keyword may
|
|
35
|
+
repeat, each optionally preceded by its own ``(*Group:...*)`` marker, so a
|
|
36
|
+
variable's group is the most recent marker seen at or before it.
|
|
37
|
+
|
|
38
|
+
Grammar per declaration::
|
|
39
|
+
|
|
40
|
+
<name> [AT <address>] : <type> ; [(*description*)]
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import re
|
|
46
|
+
import struct
|
|
47
|
+
from dataclasses import dataclass, field
|
|
48
|
+
|
|
49
|
+
from .errors import UnsupportedFormat
|
|
50
|
+
|
|
51
|
+
#: Block keywords that may introduce a declaration list.
|
|
52
|
+
BLOCK_KEYWORDS = (
|
|
53
|
+
"VAR_GLOBAL",
|
|
54
|
+
"VAR_EXTERNAL",
|
|
55
|
+
"VAR_INPUT",
|
|
56
|
+
"VAR_OUTPUT",
|
|
57
|
+
"VAR_IN_OUT",
|
|
58
|
+
"VAR_TEMP",
|
|
59
|
+
"VAR_RETAIN",
|
|
60
|
+
"VAR_CONFIG",
|
|
61
|
+
"VAR_ACCESS",
|
|
62
|
+
"VAR",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_GROUP_RE = re.compile(
|
|
66
|
+
r"^\(\*Group\s*:\s*(?P<inner>.*)\*\)\s*$",
|
|
67
|
+
re.IGNORECASE | re.DOTALL,
|
|
68
|
+
)
|
|
69
|
+
_TRAILING_COMMENT_RE = re.compile(r"\(\*(.*?)\*\)\s*$", re.DOTALL)
|
|
70
|
+
_ADDRESS_RE = re.compile(r"^(.*?)\s+AT\s+(\S+)$", re.IGNORECASE | re.DOTALL)
|
|
71
|
+
|
|
72
|
+
#: Keywords that may follow a block keyword without introducing a declaration.
|
|
73
|
+
#: ``VAR_GLOBAL RETAIN`` is real MotionWorks output; ``RETAIN`` is a modifier,
|
|
74
|
+
#: not a variable name.
|
|
75
|
+
_BLOCK_MODIFIERS = {
|
|
76
|
+
"RETAIN", "NON_RETAIN", "CONSTANT", "PERSISTENT", "VAR_INPUT", "VAR_OUTPUT",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#: Usage values observed in the .VGR binary grid, for cross-checking.
|
|
80
|
+
USAGE_LOCAL = 1
|
|
81
|
+
USAGE_EXTERNAL = 5
|
|
82
|
+
USAGE_FB_INSTANCE = 0x00040001
|
|
83
|
+
USAGE_NAMES = {
|
|
84
|
+
USAGE_LOCAL: "local",
|
|
85
|
+
USAGE_EXTERNAL: "external",
|
|
86
|
+
USAGE_FB_INSTANCE: "fb-instance",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#: Block keyword -> whether it declares data owned by this POU.
|
|
90
|
+
_EXTERNAL_BLOCKS = {"VAR_EXTERNAL"}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Variable:
|
|
95
|
+
"""A single declared variable."""
|
|
96
|
+
|
|
97
|
+
name: str
|
|
98
|
+
type_name: str
|
|
99
|
+
section: str
|
|
100
|
+
group: str | None = None
|
|
101
|
+
address: str | None = None
|
|
102
|
+
description: str | None = None
|
|
103
|
+
line: int = 0
|
|
104
|
+
initial_value: str | None = None
|
|
105
|
+
disabled: bool = False
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def is_external(self) -> bool:
|
|
109
|
+
"""True when the POU references a symbol owned elsewhere."""
|
|
110
|
+
return self.section in _EXTERNAL_BLOCKS
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def is_global(self) -> bool:
|
|
114
|
+
return self.section == "VAR_GLOBAL"
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def base_type(self) -> str:
|
|
118
|
+
"""The type with any array/string qualification reduced.
|
|
119
|
+
|
|
120
|
+
``ARRAY [0..7] OF INT`` reduces to ``ARRAY`` and ``STRING[80]`` to
|
|
121
|
+
``STRING``, so classification is not fooled by parameters.
|
|
122
|
+
"""
|
|
123
|
+
text = self.type_name.strip()
|
|
124
|
+
upper = text.upper()
|
|
125
|
+
if upper.startswith("ARRAY"):
|
|
126
|
+
return "ARRAY"
|
|
127
|
+
if "[" in text:
|
|
128
|
+
text = text.split("[", 1)[0].strip()
|
|
129
|
+
if "." in text:
|
|
130
|
+
# A dotted type such as a library structure reference.
|
|
131
|
+
text = text.rsplit(".", 1)[-1].strip()
|
|
132
|
+
return text
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_fb_instance(self) -> bool:
|
|
136
|
+
"""True when the type looks like a function-block instance.
|
|
137
|
+
|
|
138
|
+
MotionWorks marks these with usage ``0x00040001`` in the binary grid.
|
|
139
|
+
A function-block instance is a *stateful* instance rather than a plain
|
|
140
|
+
value, which matters when transplanting bodies between POUs. The check
|
|
141
|
+
is case-insensitive and ignores any default initialiser, because a
|
|
142
|
+
declaration such as ``BOOL := FALSE`` is a plain value.
|
|
143
|
+
"""
|
|
144
|
+
return (
|
|
145
|
+
not self.is_global
|
|
146
|
+
and not self.is_external
|
|
147
|
+
and self.base_type.upper() not in BUILTIN_TYPES
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def to_dict(self) -> dict[str, object]:
|
|
151
|
+
return {
|
|
152
|
+
"name": self.name,
|
|
153
|
+
"type": self.type_name,
|
|
154
|
+
"section": self.section,
|
|
155
|
+
"group": self.group,
|
|
156
|
+
"address": self.address,
|
|
157
|
+
"initial_value": self.initial_value,
|
|
158
|
+
"description": self.description,
|
|
159
|
+
"line": self.line,
|
|
160
|
+
**({"disabled": True} if self.disabled else {}),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def format_line(self) -> str:
|
|
164
|
+
bits = [self.name]
|
|
165
|
+
if self.address:
|
|
166
|
+
bits.append(f"AT {self.address}")
|
|
167
|
+
text = f"{' '.join(bits)} : {self.type_name}"
|
|
168
|
+
if self.initial_value:
|
|
169
|
+
text += f" := {self.initial_value}"
|
|
170
|
+
if self.description:
|
|
171
|
+
text += f" (*{self.description}*)"
|
|
172
|
+
return text
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class DeclarationTable:
|
|
177
|
+
"""Every variable declared in one ``.VB`` stream."""
|
|
178
|
+
|
|
179
|
+
variables: list[Variable] = field(default_factory=list)
|
|
180
|
+
warnings: list[str] = field(default_factory=list)
|
|
181
|
+
source_stream: str | None = None
|
|
182
|
+
|
|
183
|
+
def __len__(self) -> int:
|
|
184
|
+
return len(self.variables)
|
|
185
|
+
|
|
186
|
+
def by_name(self, name: str) -> Variable | None:
|
|
187
|
+
target = name.casefold()
|
|
188
|
+
for v in self.variables:
|
|
189
|
+
if v.name.casefold() == target:
|
|
190
|
+
return v
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def sections(self) -> dict[str, int]:
|
|
194
|
+
counts: dict[str, int] = {}
|
|
195
|
+
for v in self.variables:
|
|
196
|
+
counts[v.section] = counts.get(v.section, 0) + 1
|
|
197
|
+
return counts
|
|
198
|
+
|
|
199
|
+
def duplicates(self) -> dict[str, list[Variable]]:
|
|
200
|
+
seen: dict[str, list[Variable]] = {}
|
|
201
|
+
for v in self.variables:
|
|
202
|
+
seen.setdefault(v.name.casefold(), []).append(v)
|
|
203
|
+
return {k: v for k, v in seen.items() if len(v) > 1}
|
|
204
|
+
|
|
205
|
+
def variants(self, name: str) -> list[Variable]:
|
|
206
|
+
"""All declarations of ``name``, in file order.
|
|
207
|
+
|
|
208
|
+
MotionWorks permits the same symbol to appear in more than one block
|
|
209
|
+
(for example declared in a ``VAR`` and referenced in a
|
|
210
|
+
``VAR_EXTERNAL``), so callers must not assume a name is unique.
|
|
211
|
+
"""
|
|
212
|
+
target = name.casefold()
|
|
213
|
+
return [v for v in self.variables if v.name.casefold() == target]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
#: IEC elementary types that are plain values rather than function-block
|
|
217
|
+
#: instances. Types deliberately *not* listed here (``AXIS_REF``,
|
|
218
|
+
#: ``TASK_INFO_ECLR``, ``CONTROLLER_INFO``, library structures) are reported as
|
|
219
|
+
#: instances, because classification only affects how a donor body is treated.
|
|
220
|
+
BUILTIN_TYPES = {
|
|
221
|
+
"BOOL", "BYTE", "WORD", "DWORD", "LWORD", "SINT", "USINT", "INT", "UINT",
|
|
222
|
+
"DINT", "UDINT", "LINT", "ULINT", "REAL", "LREAL", "STRING", "WSTRING",
|
|
223
|
+
"TIME", "LTIME", "DATE", "LDATE", "TOD", "LTOD", "DT", "LDT", "CHAR",
|
|
224
|
+
"WCHAR", "TIME_OF_DAY", "DATE_AND_TIME", "ARRAY",
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _split_type_initial_comment(body: str) -> tuple[str, str | None, str | None]:
|
|
229
|
+
"""Split ``TYPE := INIT;(*desc*)`` into type, initial value, and description."""
|
|
230
|
+
description = None
|
|
231
|
+
match = _TRAILING_COMMENT_RE.search(body)
|
|
232
|
+
if match:
|
|
233
|
+
description = match.group(1).strip()
|
|
234
|
+
body = body[: match.start()]
|
|
235
|
+
body = body.strip().rstrip(";").strip()
|
|
236
|
+
|
|
237
|
+
# Native publication/PLC flags follow the initializer/type as brace attributes.
|
|
238
|
+
# Strip only these observed attributes outside strings; they are not IEC value
|
|
239
|
+
# text. Native flag read-back remains the authority for its boolean state.
|
|
240
|
+
attribute = re.search(r'\s*\{(?:PDD|CSV|NOP|RDT)(?:\s*,\s*(?:PDD|CSV|NOP|RDT))*\}\s*$', body, re.I)
|
|
241
|
+
if attribute:
|
|
242
|
+
quote = None
|
|
243
|
+
pos = 0
|
|
244
|
+
while pos < attribute.start():
|
|
245
|
+
char = body[pos]
|
|
246
|
+
if quote:
|
|
247
|
+
if char == '$': pos += 2; continue
|
|
248
|
+
if char == quote:
|
|
249
|
+
if pos+1 < attribute.start() and body[pos+1] == quote: pos += 2; continue
|
|
250
|
+
quote = None
|
|
251
|
+
elif char in "'\"": quote = char
|
|
252
|
+
pos += 1
|
|
253
|
+
if quote is None:
|
|
254
|
+
body = body[:attribute.start()].rstrip()
|
|
255
|
+
|
|
256
|
+
initial_value = None
|
|
257
|
+
if ":=" in body:
|
|
258
|
+
body, initial_value = body.split(":=", 1)
|
|
259
|
+
initial_value = initial_value.strip()
|
|
260
|
+
return body.strip(), initial_value, description
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def parse_declarations(text: str, source_stream: str | None = None) -> DeclarationTable:
|
|
264
|
+
"""Parse a ``.VB`` declaration stream into a :class:`DeclarationTable`.
|
|
265
|
+
|
|
266
|
+
The parser is tolerant: a line it cannot interpret becomes a warning rather
|
|
267
|
+
than an exception, because a partially understood file is still useful for
|
|
268
|
+
reading and a hard failure would hide the rest of the project.
|
|
269
|
+
"""
|
|
270
|
+
table = DeclarationTable(source_stream=source_stream)
|
|
271
|
+
section: str | None = None
|
|
272
|
+
group: str | None = None
|
|
273
|
+
|
|
274
|
+
for lineno, raw_line in enumerate(text.splitlines(), start=1):
|
|
275
|
+
line = raw_line.strip()
|
|
276
|
+
disabled = False
|
|
277
|
+
# The IDE retains disabled declarations as (*<declaration;>*) followed
|
|
278
|
+
# by their description. Recognize that exact wrapper, not ordinary
|
|
279
|
+
# comments, and retain its disabled state for saved-source readers.
|
|
280
|
+
wrapped = re.fullmatch(r'\(\*<(.*?)>\*\)(\s*(?:\(\*.*?\*\))?)', line)
|
|
281
|
+
if wrapped:
|
|
282
|
+
line = wrapped[1] + wrapped[2]
|
|
283
|
+
disabled = True
|
|
284
|
+
|
|
285
|
+
group_match = _GROUP_RE.match(line)
|
|
286
|
+
if group_match:
|
|
287
|
+
group = group_match.group("inner").strip()
|
|
288
|
+
# A group marker is always alone on its line in practice, so once it
|
|
289
|
+
# is recognised the whole line is consumed.
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
if not line:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
upper = line.upper()
|
|
296
|
+
|
|
297
|
+
if upper == "END_VAR":
|
|
298
|
+
section = None
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
# Match the longest keyword first so VAR_EXTERNAL never matches VAR.
|
|
302
|
+
matched_block = None
|
|
303
|
+
for keyword in BLOCK_KEYWORDS:
|
|
304
|
+
if upper == keyword or upper.startswith(keyword + " "):
|
|
305
|
+
# Guard against "VAR_EXTERNAL" being caught by a prefix rule.
|
|
306
|
+
matched_block = keyword
|
|
307
|
+
break
|
|
308
|
+
if matched_block:
|
|
309
|
+
section = matched_block
|
|
310
|
+
remainder = line[len(matched_block):].strip()
|
|
311
|
+
if not remainder or remainder.upper().rstrip() in _BLOCK_MODIFIERS:
|
|
312
|
+
# "VAR_GLOBAL RETAIN" declares nothing on this line.
|
|
313
|
+
continue
|
|
314
|
+
line = remainder
|
|
315
|
+
upper = line.upper()
|
|
316
|
+
|
|
317
|
+
if section is None:
|
|
318
|
+
table.warnings.append(
|
|
319
|
+
f"line {lineno}: declaration outside any VAR block: {line!r}"
|
|
320
|
+
)
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
if ":" not in line:
|
|
324
|
+
table.warnings.append(f"line {lineno}: no ':' in declaration: {line!r}")
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
left, right = line.split(":", 1)
|
|
328
|
+
type_name, initial_value, description = _split_type_initial_comment(right)
|
|
329
|
+
if not type_name:
|
|
330
|
+
table.warnings.append(f"line {lineno}: empty type in {line!r}")
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
left = left.strip()
|
|
334
|
+
address = None
|
|
335
|
+
address_match = _ADDRESS_RE.match(left)
|
|
336
|
+
if address_match:
|
|
337
|
+
left = address_match.group(1).strip()
|
|
338
|
+
address = address_match.group(2).strip()
|
|
339
|
+
|
|
340
|
+
if not left:
|
|
341
|
+
table.warnings.append(f"line {lineno}: empty variable name in {line!r}")
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
# A left side such as "A, B" declares several names of one type.
|
|
345
|
+
for name in (n.strip() for n in left.split(",")):
|
|
346
|
+
if not name:
|
|
347
|
+
continue
|
|
348
|
+
table.variables.append(
|
|
349
|
+
Variable(
|
|
350
|
+
name=name,
|
|
351
|
+
type_name=type_name,
|
|
352
|
+
section=section,
|
|
353
|
+
group=group,
|
|
354
|
+
address=address,
|
|
355
|
+
description=description,
|
|
356
|
+
line=lineno,
|
|
357
|
+
initial_value=initial_value,
|
|
358
|
+
disabled=disabled,
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if section is not None:
|
|
363
|
+
table.warnings.append(f"unterminated {section} block at end of file")
|
|
364
|
+
|
|
365
|
+
for name, dupes in table.duplicates().items():
|
|
366
|
+
table.warnings.append(
|
|
367
|
+
f"variable {name!r} declared {len(dupes)} times "
|
|
368
|
+
f"(lines {', '.join(str(d.line) for d in dupes)})"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return table
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Native record operations are centralized so duplicate definitions cannot diverge.
|
|
375
|
+
from .grid import parse as parse_grid_records, read_record as read_grid_record
|
|
376
|
+
from .grid import add as append_grid_variable, edit as update_grid_variable, delete as remove_grid_variable
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def default_initial_value(type_name: str) -> str:
|
|
380
|
+
return {"BOOL": "FALSE", "REAL": "0.0", "LREAL": "0.0", "TIME": "T#0s"}.get(type_name.upper(), "0")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def read_grid_variable_count(grid: bytes) -> int | None:
|
|
384
|
+
"""Return the variable count recorded in a ``.VGR`` binary grid header.
|
|
385
|
+
|
|
386
|
+
Offsets 0 and 4 hold format markers; the record count is a little-endian
|
|
387
|
+
DWORD at offset 8. This is used to cross-check the textual declaration
|
|
388
|
+
count, which is how a mis-parse or a desynchronised grid is detected.
|
|
389
|
+
"""
|
|
390
|
+
if len(grid) < 12:
|
|
391
|
+
return None
|
|
392
|
+
return struct.unpack_from("<I", grid, 8)[0]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
#: Magic observed at the start of MotionWorks' compressed stream container.
|
|
396
|
+
#: The body that follows is not a standard codec (zlib/deflate, LZMA and bzip2
|
|
397
|
+
#: all fail on it), so these streams are recognised and reported, not decoded.
|
|
398
|
+
COMPRESSED_MAGIC = bytes.fromhex("CADAC758")
|
|
399
|
+
|
|
400
|
+
#: Sidecar suffix holding a 64-byte digest plus an 8-byte header.
|
|
401
|
+
SIDECAR_SUFFIX = ".sn"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def is_compressed_stream(payload: bytes) -> bool:
|
|
405
|
+
"""True when a stream uses the MotionWorks compressed container.
|
|
406
|
+
|
|
407
|
+
The magic is checked against the *whole* compact payload because the first
|
|
408
|
+
eight bytes are a little-endian hash/length header on some streams, which
|
|
409
|
+
shifts the magic off offset zero.
|
|
410
|
+
"""
|
|
411
|
+
head = payload[:16]
|
|
412
|
+
return COMPRESSED_MAGIC in head or head[:4] == b"\x00\x00\x00\x00" and not looks_like_text(head)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def looks_like_text(payload: bytes) -> bool:
|
|
416
|
+
"""Heuristic: is this payload plausibly IEC 61131-3 source text?"""
|
|
417
|
+
if not payload:
|
|
418
|
+
return True
|
|
419
|
+
sample = payload[:4096]
|
|
420
|
+
printable = sum(
|
|
421
|
+
1 for b in sample if 32 <= b < 127 or b in (9, 10, 13)
|
|
422
|
+
)
|
|
423
|
+
return printable / len(sample) > 0.90
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class CompressedStreamError(UnsupportedFormat):
|
|
427
|
+
"""Raised when a declaration stream uses MotionWorks' compressed container.
|
|
428
|
+
|
|
429
|
+
Five of the largest RotaryKnife POUs serialise ``<POU>V.VB`` this way. The
|
|
430
|
+
format is not a recognised codec, so rather than guess (and risk writing
|
|
431
|
+
back a corrupted project) callers are told exactly what was found.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def decode_declarations(payload: bytes, source_stream: str | None = None) -> DeclarationTable:
|
|
436
|
+
"""Decode a ``.VB`` payload, raising a precise error when it is compressed.
|
|
437
|
+
|
|
438
|
+
Kept separate from :func:`parse_declarations` so callers that already know
|
|
439
|
+
they hold text can skip the sniffing.
|
|
440
|
+
"""
|
|
441
|
+
if is_compressed_stream(payload):
|
|
442
|
+
raise CompressedStreamError(
|
|
443
|
+
f"Declaration stream {source_stream or '<unnamed>'!r} uses the "
|
|
444
|
+
f"MotionWorks compressed container (magic {COMPRESSED_MAGIC.hex()}). "
|
|
445
|
+
f"This is not a standard codec and is not yet supported; the "
|
|
446
|
+
f"variables for this POU cannot be read or edited safely."
|
|
447
|
+
)
|
|
448
|
+
if not looks_like_text(payload):
|
|
449
|
+
raise UnsupportedFormat(
|
|
450
|
+
f"Declaration stream {source_stream or '<unnamed>'!r} is neither "
|
|
451
|
+
f"valid IEC text nor the known compressed container "
|
|
452
|
+
f"(first bytes: {payload[:8].hex(' ')})"
|
|
453
|
+
)
|
|
454
|
+
return parse_declarations(payload.decode("latin1"), source_stream=source_stream)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def cross_check(table: DeclarationTable, grid_count: int | None) -> list[str]:
|
|
458
|
+
"""Compare the textual declaration count against the binary grid count.
|
|
459
|
+
|
|
460
|
+
A mismatch means the two stores disagree, which MotionWorks will surface as
|
|
461
|
+
inconsistent symbols. It is reported rather than raised so the read path
|
|
462
|
+
still returns useful data.
|
|
463
|
+
"""
|
|
464
|
+
notes: list[str] = []
|
|
465
|
+
if grid_count is None:
|
|
466
|
+
return notes
|
|
467
|
+
if grid_count != len(table):
|
|
468
|
+
notes.append(
|
|
469
|
+
f"declaration count mismatch: .VB declares {len(table)} variables "
|
|
470
|
+
f"but the .VGR grid header records {grid_count}"
|
|
471
|
+
)
|
|
472
|
+
return notes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Read-only readiness and source evidence; never repairs or restages a project."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .cfb import CompoundFile
|
|
10
|
+
from .mwt_bind import embedded_paths
|
|
11
|
+
from .staging import assert_staged, assert_proven, assert_direct_attached, StagingRefused, workspace_root
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def source_manifest(root: Path) -> dict:
|
|
15
|
+
"""Hash real native sources, not generated DLLs or tmp.sto compiler caches."""
|
|
16
|
+
root = Path(root).resolve()
|
|
17
|
+
files = {}
|
|
18
|
+
streams = {}
|
|
19
|
+
for path in sorted(root.rglob('src.st1')):
|
|
20
|
+
if not path.resolve().is_relative_to(root):
|
|
21
|
+
raise ValueError(f'Linked source escapes project: {path}')
|
|
22
|
+
relative = path.relative_to(root).as_posix()
|
|
23
|
+
files[relative] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
24
|
+
cfb = CompoundFile(path)
|
|
25
|
+
streams[relative] = {
|
|
26
|
+
name: hashlib.sha256(cfb.read_stream(name)).hexdigest()
|
|
27
|
+
for name in sorted(cfb.stream_names())
|
|
28
|
+
}
|
|
29
|
+
for name in ('LIST.POU', 'PROJECT.INF'):
|
|
30
|
+
path = root / name
|
|
31
|
+
if path.is_file():
|
|
32
|
+
if not path.resolve().is_relative_to(root):
|
|
33
|
+
raise ValueError(f'Linked source escapes project: {path}')
|
|
34
|
+
files[name] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
35
|
+
# Description text lives outside src.st1. Losing a translation must fail
|
|
36
|
+
# persistence verification even when its binary description ID is unchanged.
|
|
37
|
+
translations = {}
|
|
38
|
+
for path in sorted(root.rglob('*Translation.xml')):
|
|
39
|
+
if not path.resolve().is_relative_to(root):
|
|
40
|
+
raise ValueError(f'Linked description escapes project: {path}')
|
|
41
|
+
relative = path.relative_to(root).as_posix()
|
|
42
|
+
translations[relative] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
43
|
+
files[relative] = translations[relative]
|
|
44
|
+
metadata = {name: value for name, value in files.items() if not name.endswith('src.st1')}
|
|
45
|
+
digest = hashlib.sha256(json.dumps({'streams': streams, 'metadata': metadata}, sort_keys=True).encode()).hexdigest()
|
|
46
|
+
# These two measured streams store expanded navigation-tree/view state.
|
|
47
|
+
# Keep their hashes as evidence, but do not confuse UI normalization with
|
|
48
|
+
# changed program sources. PROJECT.TRE is deliberately NOT excluded.
|
|
49
|
+
persistence_streams = {file: {name: value for name, value in store.items()
|
|
50
|
+
if name.upper() not in ('PRMVIEWALL.DAT', 'PRMVIEWHARD.DAT')}
|
|
51
|
+
for file, store in streams.items()}
|
|
52
|
+
persistence_digest = hashlib.sha256(json.dumps({'streams': persistence_streams,
|
|
53
|
+
'metadata': metadata}, sort_keys=True).encode()).hexdigest()
|
|
54
|
+
programs = {file: {name: value for name, value in store.items()
|
|
55
|
+
if name.upper().endswith(('.VB', '.VGR', '.STB', '.GB', '.TXT'))}
|
|
56
|
+
for file, store in streams.items()}
|
|
57
|
+
program_digest = hashlib.sha256(json.dumps({'programs': programs, 'translations': translations}, sort_keys=True).encode()).hexdigest()
|
|
58
|
+
return {'project': str(root), 'source_digest': digest, 'files': files,
|
|
59
|
+
'persistence_digest': persistence_digest,
|
|
60
|
+
'program_digest': program_digest,
|
|
61
|
+
'streams': streams, 'verification': 'native_source_only',
|
|
62
|
+
'note': 'Source hashes are not compile, download, or motion evidence.'}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check(project: str) -> dict:
|
|
66
|
+
from .project import Project
|
|
67
|
+
from .validation import validate
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
root = assert_staged(project)
|
|
71
|
+
direct = False
|
|
72
|
+
except StagingRefused:
|
|
73
|
+
root = assert_direct_attached(project)
|
|
74
|
+
direct = True
|
|
75
|
+
if root.suffix.lower() == '.mwt':
|
|
76
|
+
root = root.with_suffix('')
|
|
77
|
+
# Diagnostic bypass is deliberately read-only, and still confines EVERY file
|
|
78
|
+
# to the stage. It is necessary to explain relocated identities, not trust them.
|
|
79
|
+
stage = workspace_root() / '.motionworks' / 'stage'
|
|
80
|
+
if (not direct and root.parent != stage.resolve()) or not root.is_dir():
|
|
81
|
+
raise ValueError('Select an authorized expanded workspace project')
|
|
82
|
+
for member in root.rglob('*'):
|
|
83
|
+
if not member.resolve().is_relative_to(root):
|
|
84
|
+
raise ValueError(f'Linked project member escapes stage: {member}')
|
|
85
|
+
blockers, warnings = [], []
|
|
86
|
+
try:
|
|
87
|
+
(assert_direct_attached if direct else assert_proven)(root)
|
|
88
|
+
identity_ok = True
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
identity_ok = False
|
|
91
|
+
blockers.append({'code': 'identity_invalid', 'detail': str(exc),
|
|
92
|
+
'next': 'Inspect the copied identity and original source. Do not restage over edits or hand-edit provenance.'})
|
|
93
|
+
wrapper = root.with_suffix('.mwt')
|
|
94
|
+
paths = []
|
|
95
|
+
try:
|
|
96
|
+
paths = embedded_paths(wrapper)
|
|
97
|
+
# Native sibling-directory wrappers legitimately store no absolute path.
|
|
98
|
+
# Use the same rule as check_mwt; do not manufacture a path for them.
|
|
99
|
+
bound = (all(Path(p).resolve() == root for p in paths) if paths else identity_ok)
|
|
100
|
+
if not bound:
|
|
101
|
+
blockers.append({'code': 'wrapper_unverified', 'detail': str(paths),
|
|
102
|
+
'next': 'Check identity first, then mw_code_wrapper_binding. Preserve stage edits; do not restage over them.'})
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
bound = False
|
|
105
|
+
blockers.append({'code': 'wrapper_unreadable', 'detail': str(exc),
|
|
106
|
+
'next': 'Preserve the wrapper and inspect its native format.'})
|
|
107
|
+
validation = validate(root)
|
|
108
|
+
if not validation.get('ok'):
|
|
109
|
+
blockers.append({'code': 'native_validation_failed', 'detail': validation,
|
|
110
|
+
'next': 'Inspect native errors before editing or opening.'})
|
|
111
|
+
p = Project(root)
|
|
112
|
+
bodies = {}
|
|
113
|
+
for pou in p.pous():
|
|
114
|
+
body = pou.st_body()
|
|
115
|
+
if body is not None:
|
|
116
|
+
bodies[pou.name] = body
|
|
117
|
+
timers = re.findall(r'PT\s*:=\s*(T#[\w.]+)', body, re.I)
|
|
118
|
+
if timers:
|
|
119
|
+
warnings.append({'code': 'timer_present', 'pou': pou.name,
|
|
120
|
+
'detail': f'Native ST timer presets: {sorted(set(timers))}. Verify timing claims in notes against source.'})
|
|
121
|
+
return {'project': str(root), 'workspace': str(workspace_root()),
|
|
122
|
+
'identity_valid': identity_ok, 'wrapper_bound': bound,
|
|
123
|
+
'wrapper_paths': paths, 'ready_for_ide_open': not blockers,
|
|
124
|
+
'binding_mode': 'embedded_path' if paths else 'native_sibling_directory',
|
|
125
|
+
'blockers': blockers, 'warnings': warnings,
|
|
126
|
+
'validation': validation, 'manifest': source_manifest(root),
|
|
127
|
+
'st_pous': list(bodies), 'evidence_level': 'offline_only',
|
|
128
|
+
'next_step': blockers[0]['next'] if blockers else
|
|
129
|
+
('Continue the already-open directly attached project; verify Build, Make, errors and Save. Close/reopen only with consent.'
|
|
130
|
+
if direct else 'Open this exact wrapper; verify Build, Make, errors, save and reopen. Never download from this tool.')}
|