@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,465 @@
|
|
|
1
|
+
"""Look at and interact with Windows GUI elements, via UI Automation.
|
|
2
|
+
|
|
3
|
+
This gives the agent hands to go with the eyes in :mod:`motionworks_iec_mcp.capture`.
|
|
4
|
+
It exists because automating MotionWorks needs it: the IDE shows a **"Use Trial"**
|
|
5
|
+
dialog on startup, and until that is dismissed the IDE never loads a project, so every
|
|
6
|
+
COM call fails. Clicking that button is not something COM can do.
|
|
7
|
+
|
|
8
|
+
Two rules shaped the design, and both come from testing rather than theory:
|
|
9
|
+
|
|
10
|
+
* **Prefer the accessibility pattern over synthetic input.** Tapping a control through
|
|
11
|
+
``InvokePattern`` is addressed by name, so it cannot miss the target the way a
|
|
12
|
+
coordinate does, and it does not depend on which window happens to be on top.
|
|
13
|
+
Raw mouse/keyboard input is the fallback, not the first choice.
|
|
14
|
+
* **Verify the target before acting.** Every lookup returns the element's control type
|
|
15
|
+
and rectangle, so a caller can confirm it found a *button* before clicking it.
|
|
16
|
+
|
|
17
|
+
These functions act on the live desktop, so they are deliberately explicit about what
|
|
18
|
+
they matched. Nothing here clicks by blind coordinate unless asked to.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UiaError(RuntimeError):
|
|
28
|
+
"""A UI element could not be found or acted on."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _automation():
|
|
32
|
+
"""The UI Automation namespaces, imported lazily.
|
|
33
|
+
|
|
34
|
+
``uiautomation`` is not required: everything here goes through ``comtypes``-free
|
|
35
|
+
.NET interop exposed by PowerShell, so the only requirement is Windows itself.
|
|
36
|
+
"""
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
#: PowerShell helper. UI Automation is reached through .NET, which is already present;
|
|
41
|
+
#: this avoids a third-party dependency for a core capability. The script writes JSON
|
|
42
|
+
#: to a file because capturing a child's stdout through a pipe truncates here.
|
|
43
|
+
_HELPER = r"""
|
|
44
|
+
param(
|
|
45
|
+
[string]$Action = 'list',
|
|
46
|
+
[string]$Out = '',
|
|
47
|
+
[string]$Match = '',
|
|
48
|
+
[string]$ControlType = '',
|
|
49
|
+
[int]$Handle = 0,
|
|
50
|
+
[string]$Text = ''
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
54
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
55
|
+
|
|
56
|
+
function Get-Root {
|
|
57
|
+
param([int]$H)
|
|
58
|
+
$auto = [System.Windows.Automation.AutomationElement]
|
|
59
|
+
if ($H -ne 0) {
|
|
60
|
+
return $auto::FromHandle([IntPtr]$H)
|
|
61
|
+
}
|
|
62
|
+
return $auto::RootElement
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function Describe-Element {
|
|
66
|
+
param($e)
|
|
67
|
+
$r = $e.Current.BoundingRectangle
|
|
68
|
+
[pscustomobject]@{
|
|
69
|
+
name = $e.Current.Name
|
|
70
|
+
type = $e.Current.ControlType.ProgrammaticName.Replace('ControlType.','')
|
|
71
|
+
automationId= $e.Current.AutomationId
|
|
72
|
+
enabled = $e.Current.IsEnabled
|
|
73
|
+
offscreen = $e.Current.IsOffscreen
|
|
74
|
+
left = [int]$r.Left
|
|
75
|
+
top = [int]$r.Top
|
|
76
|
+
width = [int]$r.Width
|
|
77
|
+
height = [int]$r.Height
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$results = New-Object System.Collections.Generic.List[object]
|
|
82
|
+
$note = ''
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
$root = Get-Root -H $Handle
|
|
86
|
+
|
|
87
|
+
if ($Action -eq 'click' -or $Action -eq 'invoke') {
|
|
88
|
+
# Find by exact name first, then fall back to a substring match.
|
|
89
|
+
$target = $null
|
|
90
|
+
foreach ($scope in @([System.Windows.Automation.TreeScope]::Descendants)) { }
|
|
91
|
+
$all = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants,
|
|
92
|
+
[System.Windows.Automation.Condition]::TrueCondition)
|
|
93
|
+
$candidates = @()
|
|
94
|
+
foreach ($e in $all) {
|
|
95
|
+
$n = $e.Current.Name
|
|
96
|
+
if (-not $n) { continue }
|
|
97
|
+
if ($n -eq $Match) { $candidates = @($e); break }
|
|
98
|
+
if ($Match -and $n.ToLower().Contains($Match.ToLower())) { $candidates += $e }
|
|
99
|
+
}
|
|
100
|
+
if (-not $candidates -or $candidates.Count -eq 0) {
|
|
101
|
+
throw "no element named or containing '$Match'"
|
|
102
|
+
}
|
|
103
|
+
$target = $candidates[0]
|
|
104
|
+
$ok = $false
|
|
105
|
+
try {
|
|
106
|
+
$p = $target.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)
|
|
107
|
+
$p.Invoke()
|
|
108
|
+
$ok = $true
|
|
109
|
+
$note = 'invoked via InvokePattern'
|
|
110
|
+
} catch { }
|
|
111
|
+
if (-not $ok) {
|
|
112
|
+
$note = 'no InvokePattern; reported rectangle for a coordinate click'
|
|
113
|
+
}
|
|
114
|
+
$results.Add((Describe-Element $target))
|
|
115
|
+
if (-not $ok) { $note += '' }
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
$all = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants,
|
|
119
|
+
[System.Windows.Automation.Condition]::TrueCondition)
|
|
120
|
+
foreach ($e in $all) {
|
|
121
|
+
$n = $e.Current.Name
|
|
122
|
+
if (-not $n) { continue }
|
|
123
|
+
$t = $e.Current.ControlType.ProgrammaticName.Replace('ControlType.','')
|
|
124
|
+
if ($ControlType -and $t -ne $ControlType) { continue }
|
|
125
|
+
$results.Add((Describe-Element $e))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
$note = if ($note) { $note } else { "found $($results.Count) element(s)" }
|
|
129
|
+
} catch {
|
|
130
|
+
$note = "error: $($_.Exception.Message)"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$payload = [pscustomobject]@{ note = $note; elements = $results }
|
|
134
|
+
$json = $payload | ConvertTo-Json -Depth 6 -Compress
|
|
135
|
+
if ($Out) { Set-Content -LiteralPath $Out -Value $json -Encoding UTF8 } else { $json }
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _run_helper(
|
|
140
|
+
action: str,
|
|
141
|
+
match: str = "",
|
|
142
|
+
control_type: str = "",
|
|
143
|
+
handle: int = 0,
|
|
144
|
+
timeout_s: float = 60.0,
|
|
145
|
+
) -> dict[str, object]:
|
|
146
|
+
"""Run the UI Automation helper and return its parsed result."""
|
|
147
|
+
import json
|
|
148
|
+
import subprocess
|
|
149
|
+
import tempfile
|
|
150
|
+
from pathlib import Path
|
|
151
|
+
|
|
152
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
153
|
+
script = Path(tmp) / "uia.ps1"
|
|
154
|
+
listing = Path(tmp) / "uia.json"
|
|
155
|
+
script.write_text(_HELPER, encoding="ascii")
|
|
156
|
+
command = [
|
|
157
|
+
"powershell.exe",
|
|
158
|
+
"-NoLogo",
|
|
159
|
+
"-NoProfile",
|
|
160
|
+
"-NonInteractive",
|
|
161
|
+
"-ExecutionPolicy",
|
|
162
|
+
"Bypass",
|
|
163
|
+
"-File",
|
|
164
|
+
str(script),
|
|
165
|
+
"-Action",
|
|
166
|
+
action,
|
|
167
|
+
"-Out",
|
|
168
|
+
str(listing),
|
|
169
|
+
"-Match",
|
|
170
|
+
match,
|
|
171
|
+
"-ControlType",
|
|
172
|
+
control_type,
|
|
173
|
+
"-Handle",
|
|
174
|
+
str(handle),
|
|
175
|
+
]
|
|
176
|
+
try:
|
|
177
|
+
subprocess.run(
|
|
178
|
+
command,
|
|
179
|
+
stdout=subprocess.DEVNULL,
|
|
180
|
+
stderr=subprocess.DEVNULL,
|
|
181
|
+
timeout=timeout_s + 60,
|
|
182
|
+
creationflags=0x08000000,
|
|
183
|
+
)
|
|
184
|
+
except Exception as exc: # noqa: BLE001
|
|
185
|
+
raise UiaError(f"could not run the UI Automation helper: {exc}") from exc
|
|
186
|
+
try:
|
|
187
|
+
return json.loads(listing.read_text(encoding="utf-8-sig"))
|
|
188
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
189
|
+
raise UiaError(f"the helper produced no usable result: {exc}") from exc
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def elements(
|
|
193
|
+
title_contains: str | None = None,
|
|
194
|
+
handle: int | None = None,
|
|
195
|
+
control_type: str = "",
|
|
196
|
+
) -> list[dict[str, object]]:
|
|
197
|
+
"""Every named UI element inside a window (or the whole desktop).
|
|
198
|
+
|
|
199
|
+
``control_type`` filters to one kind, e.g. ``"Button"`` -- which is how you find
|
|
200
|
+
something clickable without reading through every label.
|
|
201
|
+
"""
|
|
202
|
+
hwnd = handle or 0
|
|
203
|
+
if title_contains and not hwnd:
|
|
204
|
+
from . import capture
|
|
205
|
+
|
|
206
|
+
window = capture.find_window(title_contains=title_contains)
|
|
207
|
+
if window is None:
|
|
208
|
+
raise UiaError(f"no visible window matching {title_contains!r}")
|
|
209
|
+
hwnd = int(window["handle"])
|
|
210
|
+
result = _run_helper("list", control_type=control_type, handle=hwnd)
|
|
211
|
+
return list(result.get("elements") or [])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def find_button(name_contains: str, title_contains: str | None = None):
|
|
215
|
+
"""The first control whose name contains ``name_contains``.
|
|
216
|
+
|
|
217
|
+
Despite the name this does **not** filter to ``ControlType.Button``, and that is
|
|
218
|
+
the whole point. Measured on the MotionWorks trial dialog, every clickable control
|
|
219
|
+
reports itself as a **``Pane``**, not a Button -- which is exactly why the original
|
|
220
|
+
trial-dismissal code, which searched for a Button named "Use Trial", never found
|
|
221
|
+
anything and the IDE sat waiting forever. Matching by name alone finds it.
|
|
222
|
+
"""
|
|
223
|
+
for element in elements(title_contains=title_contains):
|
|
224
|
+
if name_contains.casefold() in str(element.get("name", "")).casefold():
|
|
225
|
+
return element
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def click_element(
|
|
230
|
+
name: str, title_contains: str | None = None, handle: int | None = None
|
|
231
|
+
) -> dict[str, object]:
|
|
232
|
+
"""Click a control by name, preferring the accessibility ``InvokePattern``.
|
|
233
|
+
|
|
234
|
+
Addresses the control by name rather than by coordinate where possible. When a
|
|
235
|
+
control exposes no invoke pattern -- as the MotionWorks dialog's ``Pane`` elements
|
|
236
|
+
do not -- it falls back to a **synthetic click at the element's own centre**, and
|
|
237
|
+
the result says which route was taken so the caller can tell them apart.
|
|
238
|
+
"""
|
|
239
|
+
hwnd = handle or 0
|
|
240
|
+
if title_contains and not hwnd:
|
|
241
|
+
from . import capture
|
|
242
|
+
|
|
243
|
+
window = capture.find_window(title_contains=title_contains)
|
|
244
|
+
if window is None:
|
|
245
|
+
raise UiaError(f"no visible window matching {title_contains!r}")
|
|
246
|
+
hwnd = int(window["handle"])
|
|
247
|
+
result = _run_helper("click", match=name, handle=hwnd)
|
|
248
|
+
matched = list(result.get("elements") or [])
|
|
249
|
+
if not matched:
|
|
250
|
+
raise UiaError(str(result.get("note", "nothing matched")))
|
|
251
|
+
element = matched[0]
|
|
252
|
+
note = str(result.get("note", ""))
|
|
253
|
+
if "InvokePattern" in note:
|
|
254
|
+
element["invoked"] = True
|
|
255
|
+
element["note"] = note
|
|
256
|
+
return element
|
|
257
|
+
|
|
258
|
+
# No invoke pattern: click the centre of the rectangle UIA reported.
|
|
259
|
+
left, top = int(element["left"]), int(element["top"])
|
|
260
|
+
width, height = int(element["width"]), int(element["height"])
|
|
261
|
+
if width <= 0 or height <= 0:
|
|
262
|
+
raise UiaError(
|
|
263
|
+
f"found {element.get('name')!r} but it has no size, so it cannot be "
|
|
264
|
+
f"clicked"
|
|
265
|
+
)
|
|
266
|
+
x, y = left + width // 2, top + height // 2
|
|
267
|
+
detail = click_point(x, y)
|
|
268
|
+
element["invoked"] = True
|
|
269
|
+
element["note"] = (
|
|
270
|
+
f"no InvokePattern, so clicked at the element centre ({x},{y}) via SendInput"
|
|
271
|
+
)
|
|
272
|
+
element["clicked_at"] = detail.get("clicked_at", (x, y))
|
|
273
|
+
return element
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _ensure_dpi_aware() -> None:
|
|
277
|
+
"""Make this process per-monitor DPI aware, once.
|
|
278
|
+
|
|
279
|
+
Without this, a click at a coordinate that UI Automation reported fails outright:
|
|
280
|
+
on this machine the trial dialog sits on a monitor with **negative x**, and an
|
|
281
|
+
unaware process asking for ``(-3227, 542)`` gets
|
|
282
|
+
``SetCursorPos: (18, 'There are no more files.')``. The coordinates UIA returns
|
|
283
|
+
are physical pixels, so the process must be aware to use them.
|
|
284
|
+
"""
|
|
285
|
+
global _DPI_SET
|
|
286
|
+
|
|
287
|
+
if _DPI_SET:
|
|
288
|
+
return
|
|
289
|
+
_DPI_SET = True
|
|
290
|
+
try:
|
|
291
|
+
import ctypes
|
|
292
|
+
|
|
293
|
+
# -4 = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (Windows 10 1703+)
|
|
294
|
+
if not ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4)):
|
|
295
|
+
# -2 = PER_MONITOR_AWARE, for older builds
|
|
296
|
+
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-2))
|
|
297
|
+
except Exception: # noqa: BLE001 - best effort; the fallback below still tries
|
|
298
|
+
try:
|
|
299
|
+
import ctypes
|
|
300
|
+
|
|
301
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
302
|
+
except Exception: # noqa: BLE001
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
_DPI_SET = False
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def click_point(x: int, y: int) -> dict[str, object]:
|
|
310
|
+
"""Synthesise a left click at an absolute desktop coordinate.
|
|
311
|
+
|
|
312
|
+
Coordinates are physical pixels across the whole virtual desktop, matching the
|
|
313
|
+
rectangles reported by :func:`elements` and
|
|
314
|
+
:func:`motionworks_iec_mcp.capture.list_windows`.
|
|
315
|
+
|
|
316
|
+
Uses ``SendInput`` with ``MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK`` rather
|
|
317
|
+
than ``SetCursorPos`` plus ``mouse_event``: the latter rejects coordinates on a
|
|
318
|
+
monitor left of the primary one, which is where the IDE's dialog sits here.
|
|
319
|
+
"""
|
|
320
|
+
if sys.platform != "win32":
|
|
321
|
+
raise UiaError("synthetic input is only implemented on Windows")
|
|
322
|
+
import ctypes
|
|
323
|
+
from ctypes import wintypes
|
|
324
|
+
|
|
325
|
+
_ensure_dpi_aware()
|
|
326
|
+
|
|
327
|
+
user32 = ctypes.windll.user32
|
|
328
|
+
virtual_left = user32.GetSystemMetrics(76) # SM_XVIRTUALSCREEN
|
|
329
|
+
virtual_top = user32.GetSystemMetrics(77)
|
|
330
|
+
virtual_width = user32.GetSystemMetrics(78)
|
|
331
|
+
virtual_height = user32.GetSystemMetrics(79)
|
|
332
|
+
if virtual_width <= 0 or virtual_height <= 0:
|
|
333
|
+
raise UiaError("could not read the virtual desktop size")
|
|
334
|
+
|
|
335
|
+
# SendInput takes coordinates normalised to 0..65535 across the virtual desktop.
|
|
336
|
+
normalised_x = int((int(x) - virtual_left) * 65535 / max(1, virtual_width - 1))
|
|
337
|
+
normalised_y = int((int(y) - virtual_top) * 65535 / max(1, virtual_height - 1))
|
|
338
|
+
|
|
339
|
+
class MOUSEINPUT(ctypes.Structure):
|
|
340
|
+
_fields_ = [
|
|
341
|
+
("dx", wintypes.LONG),
|
|
342
|
+
("dy", wintypes.LONG),
|
|
343
|
+
("mouseData", wintypes.DWORD),
|
|
344
|
+
("dwFlags", wintypes.DWORD),
|
|
345
|
+
("time", wintypes.DWORD),
|
|
346
|
+
("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
class INPUT(ctypes.Structure):
|
|
350
|
+
class _UNION(ctypes.Union):
|
|
351
|
+
_fields_ = [("mi", MOUSEINPUT)]
|
|
352
|
+
|
|
353
|
+
_anonymous_ = ("u",)
|
|
354
|
+
_fields_ = [("type", wintypes.DWORD), ("u", _UNION)]
|
|
355
|
+
|
|
356
|
+
MOUSEEVENTF_MOVE = 0x0001
|
|
357
|
+
MOUSEEVENTF_LEFTDOWN = 0x0002
|
|
358
|
+
MOUSEEVENTF_LEFTUP = 0x0004
|
|
359
|
+
MOUSEEVENTF_ABSOLUTE = 0x8000
|
|
360
|
+
MOUSEEVENTF_VIRTUALDESK = 0x4000
|
|
361
|
+
flags = (
|
|
362
|
+
MOUSEEVENTF_MOVE
|
|
363
|
+
| MOUSEEVENTF_ABSOLUTE
|
|
364
|
+
| MOUSEEVENTF_VIRTUALDESK
|
|
365
|
+
| MOUSEEVENTF_LEFTDOWN
|
|
366
|
+
| MOUSEEVENTF_LEFTUP
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
events = (INPUT * 1)()
|
|
370
|
+
events[0].type = 0 # INPUT_MOUSE
|
|
371
|
+
events[0].mi = MOUSEINPUT(
|
|
372
|
+
normalised_x, normalised_y, 0, flags, 0, None
|
|
373
|
+
)
|
|
374
|
+
sent = user32.SendInput(1, ctypes.byref(events), ctypes.sizeof(INPUT))
|
|
375
|
+
if sent != 1:
|
|
376
|
+
raise UiaError(
|
|
377
|
+
f"SendInput sent {sent} of 1 event; clicking at ({x},{y}) failed"
|
|
378
|
+
)
|
|
379
|
+
return {"clicked_at": (int(x), int(y)), "sent": int(sent)}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def type_text(text: str, interval: float = 0.01) -> None:
|
|
383
|
+
"""Type text into whatever window has focus."""
|
|
384
|
+
if sys.platform != "win32":
|
|
385
|
+
raise UiaError("synthetic input is only implemented on Windows")
|
|
386
|
+
import win32api
|
|
387
|
+
import win32con
|
|
388
|
+
|
|
389
|
+
for character in text:
|
|
390
|
+
code = win32api.VkKeyScan(character)
|
|
391
|
+
if code == -1:
|
|
392
|
+
continue
|
|
393
|
+
virtual_key = code & 0xFF
|
|
394
|
+
shift = bool(code >> 8 & 1)
|
|
395
|
+
keys = [virtual_key]
|
|
396
|
+
if shift:
|
|
397
|
+
win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)
|
|
398
|
+
win32api.keybd_event(virtual_key, 0, 0, 0)
|
|
399
|
+
win32api.keybd_event(virtual_key, 0, win32con.KEYEVENTF_KEYUP, 0)
|
|
400
|
+
if shift:
|
|
401
|
+
win32api.keybd_event(win32con.VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0)
|
|
402
|
+
time.sleep(interval)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def dismiss_trial(title_contains: str = "Motionworks", timeout: float = 45.0):
|
|
406
|
+
"""Click **Use Trial** if the trial dialog is showing, and say what happened.
|
|
407
|
+
|
|
408
|
+
This is the single thing that stood between the IDE and every COM call working.
|
|
409
|
+
While this dialog is up, MotionWorks never loads a project, so ``OpenProject`` and
|
|
410
|
+
``IsProjectOpen`` both return ``Internal error`` -- which looks like a broken
|
|
411
|
+
install but is only an unanswered prompt.
|
|
412
|
+
|
|
413
|
+
Returns ``(dismissed, detail)``. Polls, because the dialog can appear a few seconds
|
|
414
|
+
after the process starts, and a single attempt at a fixed moment misses it.
|
|
415
|
+
"""
|
|
416
|
+
deadline = time.monotonic() + timeout
|
|
417
|
+
seen = False
|
|
418
|
+
last = ""
|
|
419
|
+
while time.monotonic() < deadline:
|
|
420
|
+
from . import capture
|
|
421
|
+
|
|
422
|
+
# Exclude browsers. A Chrome tab can be titled with the application name and
|
|
423
|
+
# is far larger than the dialog, so the "largest match" rule would pick it and
|
|
424
|
+
# the dialog would never be found -- which is exactly what happened.
|
|
425
|
+
window = capture.find_window(
|
|
426
|
+
title_contains=title_contains,
|
|
427
|
+
exclude=("chrome", "edge", "firefox", "brave", "opera"),
|
|
428
|
+
)
|
|
429
|
+
windows = [window] if window else []
|
|
430
|
+
if windows:
|
|
431
|
+
handle = int(windows[0]["handle"])
|
|
432
|
+
try:
|
|
433
|
+
for element in elements(handle=handle):
|
|
434
|
+
name = str(element.get("name", ""))
|
|
435
|
+
if "use trial" in name.casefold():
|
|
436
|
+
seen = True
|
|
437
|
+
clicked = click_element(handle=handle, name="Use Trial")
|
|
438
|
+
return True, (
|
|
439
|
+
f"clicked {clicked.get('name')!r}: {clicked.get('note')}"
|
|
440
|
+
)
|
|
441
|
+
except UiaError as exc:
|
|
442
|
+
last = str(exc)
|
|
443
|
+
time.sleep(1.5)
|
|
444
|
+
if seen:
|
|
445
|
+
return False, f"the trial dialog was found but could not be clicked: {last}"
|
|
446
|
+
return False, (
|
|
447
|
+
f"no window matching {title_contains!r} appeared within {timeout:.0f}s"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def describe(elements_list: list[dict[str, object]], limit: int = 40) -> str:
|
|
452
|
+
"""A readable listing of UI elements, for a tool reply."""
|
|
453
|
+
if not elements_list:
|
|
454
|
+
return "No named UI elements found."
|
|
455
|
+
lines = [f"{min(len(elements_list), limit)} of {len(elements_list)} element(s):"]
|
|
456
|
+
for element in elements_list[:limit]:
|
|
457
|
+
name = str(element.get("name", ""))[:60]
|
|
458
|
+
kind = element.get("type", "?")
|
|
459
|
+
left, top = element.get("left"), element.get("top")
|
|
460
|
+
width, height = element.get("width"), element.get("height")
|
|
461
|
+
lines.append(
|
|
462
|
+
f" [{kind:<12}] {name!r:<64} at ({left},{top}) {width}x{height}"
|
|
463
|
+
+ ("" if element.get("enabled", True) else " DISABLED")
|
|
464
|
+
)
|
|
465
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Offline consistency evidence; never a substitute for IDE acceptance."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from .cfb import CompoundFile
|
|
4
|
+
from .grid import validate_pair
|
|
5
|
+
from .tree import parse_document
|
|
6
|
+
from .variable_edit import check_addresses
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate(project, *, containers=None):
|
|
10
|
+
root = Path(project)
|
|
11
|
+
if root.suffix.lower() == '.mwt': root = root.with_suffix('')
|
|
12
|
+
checked, errors = [], []
|
|
13
|
+
sources = list(root.rglob('src.st1')) if containers is None else containers
|
|
14
|
+
for source in sources:
|
|
15
|
+
source = Path(source)
|
|
16
|
+
label = source.relative_to(root).as_posix()
|
|
17
|
+
try:
|
|
18
|
+
cfb = CompoundFile(source)
|
|
19
|
+
names = cfb.stream_names()
|
|
20
|
+
for name in names:
|
|
21
|
+
# Read every stream to check allocation/readability too.
|
|
22
|
+
raw = cfb.read_stream(name)
|
|
23
|
+
if name.upper().endswith('.VB'):
|
|
24
|
+
grid = next((n for n in names if n.casefold() == (name[:-3]+'.VGR').casefold()), None)
|
|
25
|
+
if grid is None: raise ValueError(f'Missing grid for {name}')
|
|
26
|
+
result = validate_pair(raw.decode('latin1'), cfb.read_stream(grid))
|
|
27
|
+
check_addresses(raw.decode('latin1'))
|
|
28
|
+
checked.append({'file': label, 'stream': name, **result})
|
|
29
|
+
elif name == 'PROJECT.TRE':
|
|
30
|
+
document = parse_document(raw.decode('latin1'))
|
|
31
|
+
nodes = [node for node, _ in document.walk_with_ancestors()]
|
|
32
|
+
if len(nodes) != int(document.lines[1]):
|
|
33
|
+
raise ValueError('PROJECT.TRE total does not match parsed nodes')
|
|
34
|
+
ids = [node.node_id for node in nodes]
|
|
35
|
+
if len(set(ids)) != len(ids): raise ValueError('Duplicate PROJECT.TRE node IDs')
|
|
36
|
+
checked.append({'file': label, 'stream': name, 'nodes': len(nodes)})
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
errors.append({'file': label, 'error': str(exc)})
|
|
39
|
+
if not sources: errors.append({'error': 'No native containers found'})
|
|
40
|
+
return {'ok': not errors, 'verification': 'offline_only', 'checked': checked, 'errors': errors,
|
|
41
|
+
'not_verified': ['IDE Rebuild', 'Make', 'save/close/reopen', 'controller behavior']}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Plan paired declaration/worksheet edits using compatible native donors."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from . import grid as G, declarations as D
|
|
6
|
+
from .cfb import CompoundFile
|
|
7
|
+
from .errors import UnsupportedFormat
|
|
8
|
+
from .variables import parse_declarations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _streams(project_root, pou):
|
|
12
|
+
from .writer import _declaration_target
|
|
13
|
+
source, vb, before, text = _declaration_target(project_root, pou)
|
|
14
|
+
cfb = CompoundFile(source)
|
|
15
|
+
vgr = vb[:-3] + '.VGR'
|
|
16
|
+
if vgr not in cfb.stream_names():
|
|
17
|
+
raise UnsupportedFormat(f'Missing paired worksheet {vgr}; refusing a text-only edit')
|
|
18
|
+
raw = cfb.read_stream(vgr)
|
|
19
|
+
# The pair's pre-existing disagreements are returned rather than raised: a project that
|
|
20
|
+
# already carries one record the vendor tooling accepts must not block work on another
|
|
21
|
+
# variable. The edit is still proven not to introduce a new one in _plan.
|
|
22
|
+
return source, vb, before, text, vgr, raw, G.pair_findings(text, raw)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _address_span(address, type_name):
|
|
26
|
+
if not address: return None
|
|
27
|
+
match = re.fullmatch(r'%([IQM])([XBWDL])(\d+)(?:\.(\d+))?', address.upper())
|
|
28
|
+
if not match: raise UnsupportedFormat(f'Unrecognized direct address {address!r}')
|
|
29
|
+
area, width, offset, bit = match.groups()
|
|
30
|
+
if width == 'X':
|
|
31
|
+
if bit is None or not 0 <= int(bit) <= 7:
|
|
32
|
+
raise UnsupportedFormat('A bit address needs a bit number from 0 to 7')
|
|
33
|
+
start, bits = int(offset) * 8 + int(bit), 1
|
|
34
|
+
else:
|
|
35
|
+
if bit is not None: raise UnsupportedFormat('Non-bit address has a bit suffix')
|
|
36
|
+
start, bits = int(offset) * 8, {'B': 8, 'W': 16, 'D': 32, 'L': 64}[width]
|
|
37
|
+
warning = D.check_address_type(address, type_name)
|
|
38
|
+
if warning: raise UnsupportedFormat(warning)
|
|
39
|
+
return area, start, start + bits
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_addresses(text):
|
|
43
|
+
spans = []
|
|
44
|
+
for variable in parse_declarations(text).variables:
|
|
45
|
+
# Runtime system memory uses segment.offset, unlike I/O byte.bit syntax.
|
|
46
|
+
# Preserve those existing bindings; never reinterpret their offsets as bits.
|
|
47
|
+
if variable.address and re.fullmatch(r'%M(?:[BWDL]\d+\.\d+|X\d+\.\d+\.[0-7])', variable.address, re.I):
|
|
48
|
+
continue
|
|
49
|
+
span = _address_span(variable.address, variable.type_name)
|
|
50
|
+
if span is None: continue
|
|
51
|
+
for other_name, other in spans:
|
|
52
|
+
if span[0] == other[0] and span[1] < other[2] and other[1] < span[2]:
|
|
53
|
+
raise UnsupportedFormat(f'Direct addresses overlap: {variable.name} and {other_name}')
|
|
54
|
+
spans.append((variable.name, span))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _plan(source, vb, before, updated, vgr, raw, notes, preexisting=()):
|
|
58
|
+
from .writer import WritePlan
|
|
59
|
+
tolerated = set(preexisting)
|
|
60
|
+
introduced = [finding for finding in G.pair_findings(updated, raw) if finding not in tolerated]
|
|
61
|
+
if introduced:
|
|
62
|
+
raise UnsupportedFormat(introduced[0])
|
|
63
|
+
if tolerated:
|
|
64
|
+
notes = list(notes) + [
|
|
65
|
+
'Pre-existing declaration/grid disagreement left untouched (not introduced by this edit): '
|
|
66
|
+
+ '; '.join(sorted(tolerated))]
|
|
67
|
+
check_addresses(updated)
|
|
68
|
+
return WritePlan(target=source, stream=vb, before=before, after=updated.encode('latin1'),
|
|
69
|
+
extra_streams={vgr: raw}, notes=notes)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _other_pou_names(project_root):
|
|
73
|
+
"""Every POU name in the project that owns a worksheet, for an automatic donor search."""
|
|
74
|
+
base = Path(project_root) / 'POE'
|
|
75
|
+
if not base.is_dir():
|
|
76
|
+
return []
|
|
77
|
+
return sorted(child.name for child in base.iterdir()
|
|
78
|
+
if child.is_dir() and (child / 'src.st1').is_file())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _compatible(records, usages, type_name, address, donor):
|
|
82
|
+
return [r for r in records if r['usage'] in usages
|
|
83
|
+
and r['type'].casefold() == type_name.casefold()
|
|
84
|
+
and bool(r['address']) == bool(address)
|
|
85
|
+
and (donor is None or r['name'].casefold() == donor.casefold())]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def add(project_root, pou_name, name, type_name, section='VAR', address=None,
|
|
89
|
+
initial_value=None, description=None, donor=None, donor_pou=None):
|
|
90
|
+
source, vb, before, text, vgr, raw, preexisting = _streams(project_root, pou_name)
|
|
91
|
+
if address: _address_span(address, type_name)
|
|
92
|
+
section = 'VAR_GLOBAL' if pou_name is None else section.upper()
|
|
93
|
+
usages = {'VAR': (1, 0x40001), 'VAR_EXTERNAL': (5,), 'VAR_GLOBAL': (6, 22)}.get(section)
|
|
94
|
+
if usages is None: raise UnsupportedFormat(f'No proven worksheet usage for {section}')
|
|
95
|
+
donor_text, donor_raw = text, raw
|
|
96
|
+
borrowed = donor_pou is not None
|
|
97
|
+
if donor_pou is not None:
|
|
98
|
+
if pou_name is None: raise UnsupportedFormat('Global donors must come from the same resource')
|
|
99
|
+
_, _, _, donor_text, _, donor_raw, _ = _streams(project_root, donor_pou)
|
|
100
|
+
donors = _compatible(G.parse(donor_raw), usages, type_name, address, donor)
|
|
101
|
+
if not donors and donor_pou is None and donor is None:
|
|
102
|
+
# A POU that declares none of this type - an empty clone, or one whose only locals are
|
|
103
|
+
# function-block instances - has no donor of its own. Borrow one from another POU in the
|
|
104
|
+
# same project rather than refusing: it is the same project, so its worksheets encode the
|
|
105
|
+
# same layout. A donor of a DIFFERENT type is deliberately not accepted here; that was
|
|
106
|
+
# tried and produced a record the project could no longer parse.
|
|
107
|
+
for candidate in _other_pou_names(project_root):
|
|
108
|
+
if candidate.casefold() == (pou_name or '').casefold():
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
_, _, _, candidate_text, _, candidate_raw, _ = _streams(project_root, candidate)
|
|
112
|
+
except UnsupportedFormat:
|
|
113
|
+
continue
|
|
114
|
+
matches = _compatible(G.parse(candidate_raw), usages, type_name, address, None)
|
|
115
|
+
if matches:
|
|
116
|
+
donor_text, donor_raw, donors, borrowed = candidate_text, candidate_raw, matches, True
|
|
117
|
+
break
|
|
118
|
+
if not donors: raise UnsupportedFormat('No native donor matches the requested type, usage and address layout')
|
|
119
|
+
if donor is None and len({(r['fields'][1:4], r['tail']) for r in donors}) != 1:
|
|
120
|
+
raise UnsupportedFormat('Native donor is ambiguous; specify donor by variable name')
|
|
121
|
+
chosen = donors[0]
|
|
122
|
+
table = parse_declarations(donor_text)
|
|
123
|
+
declaration = next(v for v in table.variables if v.name.casefold() == chosen['name'].casefold())
|
|
124
|
+
# Inherit the donor initializer explicitly in both stores, never silently.
|
|
125
|
+
if initial_value is None:
|
|
126
|
+
initial_value = chosen['initial_value'] or None
|
|
127
|
+
new_grid, handle = G.add(raw, name, type_name, usage=chosen['usage'], address=address or '',
|
|
128
|
+
initial_value=initial_value, donor_name=chosen['name'],
|
|
129
|
+
donor_grid=donor_raw if borrowed else None, group=chosen['group'])
|
|
130
|
+
updated, notes = D.add_variable(text, name, type_name, section=section, address=address,
|
|
131
|
+
initial_value=initial_value, description=description,
|
|
132
|
+
group=declaration.group if not borrowed else None)
|
|
133
|
+
if borrowed and donor_pou is None:
|
|
134
|
+
notes = list(notes) + [
|
|
135
|
+
f'Borrowed the native record layout of {chosen["name"]} ({type_name}) from another POU '
|
|
136
|
+
'in this project, because the target POU declares no donor of that type']
|
|
137
|
+
plan = _plan(source, vb, before, updated, vgr, new_grid,
|
|
138
|
+
notes + [f'Cloned native donor {chosen["name"]}; initializer={initial_value!r}; handle={handle}; native trailer preserved'],
|
|
139
|
+
preexisting=preexisting)
|
|
140
|
+
from .descriptions import attach
|
|
141
|
+
if description is not None:
|
|
142
|
+
return attach(plan, vgr, name, description)
|
|
143
|
+
# A new variable must not inherit another variable's description ID.
|
|
144
|
+
import struct
|
|
145
|
+
new_raw = bytearray(plan.extra_streams[vgr])
|
|
146
|
+
item = G.find(G.parse(bytes(new_raw)), name)
|
|
147
|
+
identifier = struct.unpack_from('<I', new_raw, item['end'] - 8)[0]
|
|
148
|
+
if identifier not in (0, 0xffffffff):
|
|
149
|
+
struct.pack_into('<I', new_raw, item['end'] - 8, 0)
|
|
150
|
+
plan.extra_streams[vgr] = bytes(new_raw)
|
|
151
|
+
return plan
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def edit(project_root, pou_name, name, new_name=None, type_name=None, address=None,
|
|
155
|
+
initial_value=None, description=None, clear_address=False, force=False, donor=None):
|
|
156
|
+
from .writer import _reference_check
|
|
157
|
+
source, vb, before, text, vgr, raw, preexisting = _streams(project_root, pou_name)
|
|
158
|
+
if address:
|
|
159
|
+
current = G.find(G.parse(raw), name)
|
|
160
|
+
_address_span(address, type_name or current['type'])
|
|
161
|
+
if new_name and new_name.casefold() != name.casefold():
|
|
162
|
+
references = _reference_check(project_root, name, None, include_named_pou=pou_name)
|
|
163
|
+
if references:
|
|
164
|
+
raise UnsupportedFormat(f'Rename would leave references in {references}; remove those references before renaming')
|
|
165
|
+
updated, notes = D.edit_variable(text, name, new_name=new_name, type_name=type_name,
|
|
166
|
+
address=address, initial_value=initial_value, description=description, clear_address=clear_address)
|
|
167
|
+
new_grid, info = G.edit(raw, name, new_name=new_name, type_name=type_name,
|
|
168
|
+
address='' if clear_address else address, initial_value=initial_value, donor_name=donor)
|
|
169
|
+
plan = _plan(source, vb, before, updated, vgr, new_grid, notes + [str(info)], preexisting=preexisting)
|
|
170
|
+
if description is not None:
|
|
171
|
+
from .descriptions import attach
|
|
172
|
+
return attach(plan, vgr, new_name or name, description)
|
|
173
|
+
return plan
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def delete(project_root, pou_name, name, force=False):
|
|
177
|
+
from .writer import _reference_check
|
|
178
|
+
source, vb, before, text, vgr, raw, preexisting = _streams(project_root, pou_name)
|
|
179
|
+
references = _reference_check(project_root, name, None, include_named_pou=pou_name)
|
|
180
|
+
if references: raise UnsupportedFormat(f'Cannot delete {name}: referenced in {references}')
|
|
181
|
+
updated, notes = D.delete_variable(text, name)
|
|
182
|
+
new_grid, info = G.delete(raw, name)
|
|
183
|
+
return _plan(source, vb, before, updated, vgr, new_grid, notes + [str(info)], preexisting=preexisting)
|