@aryacode/dsh-engineering 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/cordis.patch.yml +122 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +77 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail readers that the engineering tools need and the legacy byte-compatible
|
|
3
|
+
* readers must not gain.
|
|
4
|
+
*
|
|
5
|
+
* The `l5x_*` tools mirror another server's JSON exactly, so a field added to
|
|
6
|
+
* `L5xTag` would break that contract. These readers answer from the same parsed
|
|
7
|
+
* document but are only used by the engineering tools: array dimensions, stored
|
|
8
|
+
* initial values, alias targets, BIT backing members and the full data-type
|
|
9
|
+
* table (which includes module-defined `AB:` types that `document.udts` omits).
|
|
10
|
+
*/
|
|
11
|
+
import { controllerElementOf } from "../model/parse.js";
|
|
12
|
+
import { descriptionOf, programElements } from "../model/module.js";
|
|
13
|
+
import { elementChildren, firstChild, nodeText, presentAttribute } from "../model/xml.js";
|
|
14
|
+
/** Scalar types whose L5K data is a single readable literal. */
|
|
15
|
+
const SCALARS = new Set(['BOOL', 'SINT', 'INT', 'DINT', 'LINT', 'USINT', 'UINT', 'UDINT', 'ULINT', 'REAL', 'LREAL']);
|
|
16
|
+
const typeIndexes = new WeakMap();
|
|
17
|
+
/** Every data type the export declares, including module-defined families. */
|
|
18
|
+
export function typeIndex(document) {
|
|
19
|
+
const cached = typeIndexes.get(document);
|
|
20
|
+
if (cached !== undefined)
|
|
21
|
+
return cached;
|
|
22
|
+
const index = new Map();
|
|
23
|
+
const dataTypes = firstChild(controllerElementOf(document), 'DataTypes');
|
|
24
|
+
if (dataTypes !== undefined) {
|
|
25
|
+
for (const type of elementChildren(dataTypes, 'DataType')) {
|
|
26
|
+
const name = type.attrs['Name']?.toLowerCase();
|
|
27
|
+
if (name === undefined || name === '')
|
|
28
|
+
continue;
|
|
29
|
+
const members = new Map();
|
|
30
|
+
const declared = firstChild(type, 'Members');
|
|
31
|
+
if (declared !== undefined) {
|
|
32
|
+
for (const member of elementChildren(declared, 'Member')) {
|
|
33
|
+
const memberName = member.attrs['Name'];
|
|
34
|
+
if (memberName === undefined || memberName === '')
|
|
35
|
+
continue;
|
|
36
|
+
const bitNumber = member.attrs['BitNumber'] === undefined ? undefined : Number.parseInt(member.attrs['BitNumber'], 10);
|
|
37
|
+
const target = presentAttribute(member, 'Target');
|
|
38
|
+
members.set(memberName.toLowerCase(), {
|
|
39
|
+
name: memberName,
|
|
40
|
+
dataType: member.attrs['DataType'] ?? '',
|
|
41
|
+
dimension: Number.parseInt(member.attrs['Dimension'] ?? '0', 10) || 0,
|
|
42
|
+
...(bitNumber === undefined || !Number.isFinite(bitNumber) ? {} : { bitNumber }),
|
|
43
|
+
...(target === undefined ? {} : { target }),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
index.set(name, members);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
typeIndexes.set(document, index);
|
|
51
|
+
return index;
|
|
52
|
+
}
|
|
53
|
+
/** One declared type's member table, by case-insensitive name. */
|
|
54
|
+
export function membersOf(document, typeName) {
|
|
55
|
+
return typeIndex(document).get(typeName.toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
/** One member of a declared type. */
|
|
58
|
+
export function memberOf(document, typeName, memberName) {
|
|
59
|
+
return membersOf(document, typeName)?.get(memberName.toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
/** Every tag element of the controller or one program, in written order. */
|
|
62
|
+
function tagElements(container) {
|
|
63
|
+
const tags = firstChild(container, 'Tags');
|
|
64
|
+
return tags === undefined ? [] : elementChildren(tags, 'Tag');
|
|
65
|
+
}
|
|
66
|
+
/** The exact `<Tag>` element for a scope and name. */
|
|
67
|
+
export function tagElement(document, scope, name) {
|
|
68
|
+
const controller = controllerElementOf(document);
|
|
69
|
+
const match = (element) => element.attrs['Name']?.toLowerCase() === name.toLowerCase();
|
|
70
|
+
if (scope.toLowerCase() === 'controller')
|
|
71
|
+
return tagElements(controller).find(match);
|
|
72
|
+
const program = programElements(controller).find(entry => (entry.attrs['Name'] ?? '').toLowerCase() === scope.toLowerCase());
|
|
73
|
+
return program === undefined ? undefined : tagElements(program).find(match);
|
|
74
|
+
}
|
|
75
|
+
/** Dimensions, stored initial value and alias target of one tag. */
|
|
76
|
+
export function tagDetail(document, tag) {
|
|
77
|
+
const element = tagElement(document, tag.scope, tag.name);
|
|
78
|
+
if (element === undefined)
|
|
79
|
+
return {};
|
|
80
|
+
const dimensions = presentAttribute(element, 'Dimensions');
|
|
81
|
+
const aliasFor = presentAttribute(element, 'AliasFor');
|
|
82
|
+
const detail = {};
|
|
83
|
+
if (dimensions !== undefined) {
|
|
84
|
+
detail.dimensions = dimensions;
|
|
85
|
+
const words = dimensions.split(/\s+/u).reduce((product, part) => product * (Number.parseInt(part, 10) || 1), 1);
|
|
86
|
+
if (Number.isFinite(words))
|
|
87
|
+
detail.words = words;
|
|
88
|
+
}
|
|
89
|
+
if (aliasFor !== undefined)
|
|
90
|
+
detail.aliasFor = aliasFor;
|
|
91
|
+
if (aliasFor === undefined && dimensions === undefined && SCALARS.has(tag.dataType.toUpperCase())) {
|
|
92
|
+
const data = elementChildren(element, 'Data').find(entry => (entry.attrs['Format'] ?? '').toUpperCase() === 'L5K')
|
|
93
|
+
?? elementChildren(element, 'Data')[0];
|
|
94
|
+
const text = data === undefined ? undefined : nodeText(data);
|
|
95
|
+
if (text !== undefined)
|
|
96
|
+
detail.value = text.trim();
|
|
97
|
+
}
|
|
98
|
+
return detail;
|
|
99
|
+
}
|
|
100
|
+
/** One UDT's visible members with BIT placement and array length. */
|
|
101
|
+
export function udtDetail(document, name) {
|
|
102
|
+
const dataTypes = firstChild(controllerElementOf(document), 'DataTypes');
|
|
103
|
+
if (dataTypes === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
const type = elementChildren(dataTypes, 'DataType').find(entry => entry.attrs['Name'] === name);
|
|
106
|
+
if (type === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
const declared = firstChild(type, 'Members');
|
|
109
|
+
const members = [];
|
|
110
|
+
if (declared !== undefined) {
|
|
111
|
+
for (const member of elementChildren(declared, 'Member')) {
|
|
112
|
+
if (member.attrs['Hidden'] === 'true')
|
|
113
|
+
continue;
|
|
114
|
+
const dimension = Number.parseInt(member.attrs['Dimension'] ?? '0', 10);
|
|
115
|
+
const description = descriptionOf(member);
|
|
116
|
+
const radix = presentAttribute(member, 'Radix');
|
|
117
|
+
const target = presentAttribute(member, 'Target');
|
|
118
|
+
const bitNumber = member.attrs['BitNumber'] === undefined ? undefined : Number.parseInt(member.attrs['BitNumber'], 10);
|
|
119
|
+
members.push({
|
|
120
|
+
name: member.attrs['Name'] ?? '',
|
|
121
|
+
dataType: member.attrs['DataType'] ?? '',
|
|
122
|
+
...(dimension > 0 ? { dimension } : {}),
|
|
123
|
+
...(description === '' ? {} : { description }),
|
|
124
|
+
...(radix === undefined ? {} : { radix }),
|
|
125
|
+
...(target === undefined ? {} : { target }),
|
|
126
|
+
...(bitNumber === undefined || !Number.isFinite(bitNumber) ? {} : { bitNumber }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { name: type.attrs['Name'] ?? name, description: descriptionOf(type), members };
|
|
131
|
+
}
|
|
132
|
+
/** Whether one declared type reaches another through nested members. */
|
|
133
|
+
function reaches(document, from, target, seen) {
|
|
134
|
+
const key = from.toLowerCase();
|
|
135
|
+
if (key === target.toLowerCase())
|
|
136
|
+
return true;
|
|
137
|
+
if (seen.has(key))
|
|
138
|
+
return false;
|
|
139
|
+
seen.add(key);
|
|
140
|
+
const members = membersOf(document, from);
|
|
141
|
+
if (members === undefined)
|
|
142
|
+
return false;
|
|
143
|
+
for (const member of members.values()) {
|
|
144
|
+
if (reaches(document, member.dataType, target, seen))
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Every tag, AOI parameter and AOI local tag whose type tree contains the type.
|
|
151
|
+
*
|
|
152
|
+
* A UDT change rewrites the serialized data of every instance, including
|
|
153
|
+
* instances of types that nest it, so the whole-project change set only accepts
|
|
154
|
+
* a member change when this list is empty.
|
|
155
|
+
*/
|
|
156
|
+
export function typeInstances(document, typeName) {
|
|
157
|
+
const instances = [];
|
|
158
|
+
for (const tag of document.tags) {
|
|
159
|
+
if (reaches(document, tag.dataType, typeName, new Set()))
|
|
160
|
+
instances.push(`${tag.scope}::${tag.name}`);
|
|
161
|
+
}
|
|
162
|
+
for (const aoi of document.aois) {
|
|
163
|
+
for (const parameter of aoi.parameters) {
|
|
164
|
+
if (reaches(document, parameter.dataType, typeName, new Set()))
|
|
165
|
+
instances.push(`AOI::${aoi.name}.${parameter.name}`);
|
|
166
|
+
}
|
|
167
|
+
for (const tag of aoi.localTags) {
|
|
168
|
+
if (reaches(document, tag.dataType, typeName, new Set()))
|
|
169
|
+
instances.push(`AOI::${aoi.name}.${tag.name}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return instances;
|
|
173
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type ProjectSnapshot } from '../session.ts';
|
|
2
|
+
import type { LogicSample } from './observations.ts';
|
|
3
|
+
/** Deployment limits for SDK-independent controller reads. */
|
|
4
|
+
export interface PollingConfig {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
minIntervalMs: number;
|
|
7
|
+
defaultIntervalMs: number;
|
|
8
|
+
maxTags: number;
|
|
9
|
+
maxSamples: number;
|
|
10
|
+
maxDurationMs: number;
|
|
11
|
+
requestTimeoutMs: number;
|
|
12
|
+
maxConcurrent: number;
|
|
13
|
+
allowedAddresses: readonly string[];
|
|
14
|
+
displayRefreshMs: number;
|
|
15
|
+
displayMaxSamples: number;
|
|
16
|
+
}
|
|
17
|
+
/** Explicit scalar operand and its project scope. */
|
|
18
|
+
export interface PollTag {
|
|
19
|
+
name: string;
|
|
20
|
+
program?: string;
|
|
21
|
+
data_type: 'BOOL' | 'INT' | 'DINT' | 'REAL';
|
|
22
|
+
}
|
|
23
|
+
/** Resolved request passed to the isolated driver. */
|
|
24
|
+
export interface PollRequest {
|
|
25
|
+
ip: string;
|
|
26
|
+
slot: number;
|
|
27
|
+
tags: PollTag[];
|
|
28
|
+
samples: number;
|
|
29
|
+
interval: number;
|
|
30
|
+
timeout: number;
|
|
31
|
+
}
|
|
32
|
+
/** Complete sampled sweep; bad values remain gaps. */
|
|
33
|
+
export interface PollSample extends LogicSample {
|
|
34
|
+
index: number;
|
|
35
|
+
overrun: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Controller identity is observed over CIP, never inferred from an export. */
|
|
38
|
+
export interface PollIdentity {
|
|
39
|
+
product_name: string;
|
|
40
|
+
serial_number: number;
|
|
41
|
+
revision: string;
|
|
42
|
+
status: number;
|
|
43
|
+
}
|
|
44
|
+
/** Validated frames retained or forwarded by the owning host. */
|
|
45
|
+
export type PollFrame = {
|
|
46
|
+
type: 'identity';
|
|
47
|
+
controller: PollIdentity;
|
|
48
|
+
} | ({
|
|
49
|
+
type: 'sample';
|
|
50
|
+
} & PollSample);
|
|
51
|
+
/** One worker's lifetime, cancelled only by its owner. */
|
|
52
|
+
export interface PollHandle {
|
|
53
|
+
cancel(): void;
|
|
54
|
+
done: Promise<PollFrame[]>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve declared scalar tags and bounds before making any network connection.
|
|
58
|
+
* @param project - Fresh export snapshot.
|
|
59
|
+
* @param input - Model arguments.
|
|
60
|
+
* @param config - Resolved deployment limits.
|
|
61
|
+
* @returns Worker request and source-linked operand evidence.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pollingRequest(project: ProjectSnapshot, input: Readonly<Record<string, unknown>>, config: PollingConfig): {
|
|
64
|
+
request: PollRequest;
|
|
65
|
+
evidence: {
|
|
66
|
+
tag: string;
|
|
67
|
+
scope: string;
|
|
68
|
+
references: import("./analysis.ts").Evidence[];
|
|
69
|
+
reference_completeness: string;
|
|
70
|
+
}[];
|
|
71
|
+
};
|
|
72
|
+
/** Validate frames crossing the worker boundary. @param value - Worker message. @returns A bounded scalar frame. */
|
|
73
|
+
export declare function decodePollFrame(value: unknown): PollFrame;
|
|
74
|
+
/** Own isolated connections across chats and await worker exit during disposal. */
|
|
75
|
+
export declare class DirectPolling {
|
|
76
|
+
private readonly workerPath;
|
|
77
|
+
readonly config: PollingConfig;
|
|
78
|
+
private readonly active;
|
|
79
|
+
private disposed;
|
|
80
|
+
constructor(workerPath: string, config: PollingConfig);
|
|
81
|
+
/**
|
|
82
|
+
* Start one bounded read window; repeated sweeps reuse its connection.
|
|
83
|
+
* @param request - Validated request.
|
|
84
|
+
* @param onFrame - Synchronous logged-output consumer.
|
|
85
|
+
* @param signal - Optional foreground lifetime.
|
|
86
|
+
* @returns Owned cancellation and quiescent completion.
|
|
87
|
+
*/
|
|
88
|
+
start(request: PollRequest, onFrame: (frame: PollFrame) => void, signal?: AbortSignal): PollHandle;
|
|
89
|
+
/** Stop only owned workers and await all exits. @returns Quiescent disposal. */
|
|
90
|
+
dispose(): Promise<void>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** Read-only EtherNet/IP sampling isolated from Studio SDK and shared host lifetimes. */
|
|
2
|
+
import { Worker } from 'node:worker_threads';
|
|
3
|
+
import { isIPv4 } from 'node:net';
|
|
4
|
+
import { EngineeringError } from "../session.js";
|
|
5
|
+
import { aliasesOf } from "./configuration.js";
|
|
6
|
+
import { findReferences } from "./analysis.js";
|
|
7
|
+
import { controllerElementOf } from "../model/parse.js";
|
|
8
|
+
import { elementChildren, firstChild } from "../model/xml.js";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve declared scalar tags and bounds before making any network connection.
|
|
11
|
+
* @param project - Fresh export snapshot.
|
|
12
|
+
* @param input - Model arguments.
|
|
13
|
+
* @param config - Resolved deployment limits.
|
|
14
|
+
* @returns Worker request and source-linked operand evidence.
|
|
15
|
+
*/
|
|
16
|
+
export function pollingRequest(project, input, config) {
|
|
17
|
+
if (!config.enabled)
|
|
18
|
+
throw new EngineeringError('DIRECT_POLLING_DISABLED', 'Enable directPollingEnabled to use SDK-independent reads');
|
|
19
|
+
const ip = input.ip_address;
|
|
20
|
+
if (typeof ip !== 'string' || !isIPv4(ip) || (config.allowedAddresses.length > 0 && !config.allowedAddresses.includes(ip)))
|
|
21
|
+
throw new EngineeringError('INVALID_CONTROLLER', 'Select an explicit IPv4 address allowed by this profile');
|
|
22
|
+
const integer = (key, fallback, min, max) => {
|
|
23
|
+
const value = input[key] ?? fallback;
|
|
24
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max)
|
|
25
|
+
throw new EngineeringError('INVALID_POLL', `${key} must be between ${min} and ${max}`);
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
const slot = integer('slot', 0, 0, 31);
|
|
29
|
+
const samples = integer('sample_count', 1, 1, config.maxSamples);
|
|
30
|
+
const interval = integer('interval_ms', config.defaultIntervalMs, config.minIntervalMs, config.maxDurationMs);
|
|
31
|
+
if ((samples - 1) * interval > config.maxDurationMs)
|
|
32
|
+
throw new EngineeringError('INVALID_POLL', 'Requested window exceeds directPollingMaxDurationMs');
|
|
33
|
+
const raw = typeof input.tags_json === 'string' ? JSON.parse(input.tags_json) : null;
|
|
34
|
+
if (!Array.isArray(raw) || raw.length < 1 || raw.length > config.maxTags)
|
|
35
|
+
throw new EngineeringError('INVALID_POLL', `tags_json must contain 1 through ${config.maxTags} declared scalar tags`);
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const tags = raw.map((entry) => {
|
|
38
|
+
if (!entry || typeof entry !== 'object' || !('name' in entry) || typeof entry.name !== 'string'
|
|
39
|
+
|| !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(entry.name) || !Object.keys(entry).every(key => key === 'name' || key === 'scope'))
|
|
40
|
+
throw new EngineeringError('INVALID_POLL', 'Each tag needs an exact scalar name and optional scope only');
|
|
41
|
+
const scope = 'scope' in entry ? entry.scope : 'controller';
|
|
42
|
+
if (typeof scope !== 'string' || (scope !== 'controller' && !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(scope)))
|
|
43
|
+
throw new EngineeringError('INVALID_POLL', 'scope must be controller or an exact program name');
|
|
44
|
+
const declared = project.document.tags.find(tag => tag.name === entry.name && tag.scope === scope);
|
|
45
|
+
const controller = controllerElementOf(project.document);
|
|
46
|
+
const programs = firstChild(controller, 'Programs');
|
|
47
|
+
const container = scope === 'controller' ? controller : programs === undefined ? undefined
|
|
48
|
+
: elementChildren(programs, 'Program').find(program => program.attrs.Name === scope);
|
|
49
|
+
const tagContainer = container === undefined ? undefined : firstChild(container, 'Tags');
|
|
50
|
+
const declaration = tagContainer === undefined ? undefined : elementChildren(tagContainer, 'Tag').find(tag => tag.attrs.Name === entry.name);
|
|
51
|
+
const dimensions = declaration?.attrs.Dimensions ?? declaration?.attrs.Dimension ?? '';
|
|
52
|
+
if (!declared || aliasesOf(project.document).has(`${scope}::${declared.name}`)
|
|
53
|
+
|| !['BOOL', 'INT', 'DINT', 'REAL'].includes(declared.dataType)
|
|
54
|
+
|| dimensions !== '' && dimensions !== '0' || declaration?.attrs.ExternalAccess === 'None')
|
|
55
|
+
throw new EngineeringError('UNSUPPORTED_POLL_TAG', 'Select a declared non-alias scalar BOOL, INT, DINT, or REAL');
|
|
56
|
+
const key = `${scope}::${declared.name}`.toLowerCase();
|
|
57
|
+
if (seen.has(key))
|
|
58
|
+
throw new EngineeringError('INVALID_POLL', 'Duplicate watch tag');
|
|
59
|
+
seen.add(key);
|
|
60
|
+
return { name: declared.name, ...(scope === 'controller' ? {} : { program: scope }), data_type: declared.dataType };
|
|
61
|
+
});
|
|
62
|
+
const request = { ip, slot, tags, samples, interval, timeout: config.requestTimeoutMs };
|
|
63
|
+
return { request, evidence: tags.map(tag => ({ tag: tag.name, scope: tag.program ?? 'controller',
|
|
64
|
+
references: findReferences(project, tag.name).filter(reference => reference.resolved?.toLowerCase() === `${tag.program ?? 'controller'}::${tag.name}`.toLowerCase()),
|
|
65
|
+
reference_completeness: 'static_partial' })) };
|
|
66
|
+
}
|
|
67
|
+
/** Validate frames crossing the worker boundary. @param value - Worker message. @returns A bounded scalar frame. */
|
|
68
|
+
export function decodePollFrame(value) {
|
|
69
|
+
if (!value || typeof value !== 'object' || !('type' in value))
|
|
70
|
+
throw new Error('Invalid polling frame');
|
|
71
|
+
if (value.type === 'failure' && 'message' in value && typeof value.message === 'string')
|
|
72
|
+
throw new Error(value.message);
|
|
73
|
+
if (value.type === 'identity' && 'controller' in value) {
|
|
74
|
+
const controller = value.controller;
|
|
75
|
+
if (controller && typeof controller === 'object' && 'product_name' in controller && typeof controller.product_name === 'string'
|
|
76
|
+
&& 'serial_number' in controller && typeof controller.serial_number === 'number' && Number.isInteger(controller.serial_number)
|
|
77
|
+
&& 'revision' in controller && typeof controller.revision === 'string' && 'status' in controller && typeof controller.status === 'number')
|
|
78
|
+
return { type: 'identity', controller: { product_name: controller.product_name, serial_number: controller.serial_number, revision: controller.revision, status: controller.status } };
|
|
79
|
+
}
|
|
80
|
+
if (value.type === 'sample' && 'index' in value && 'elapsed_ms' in value && 'sweep_ms' in value && 'overrun' in value && 'values' in value
|
|
81
|
+
&& typeof value.index === 'number' && Number.isInteger(value.index) && value.index >= 0
|
|
82
|
+
&& typeof value.elapsed_ms === 'number' && Number.isFinite(value.elapsed_ms) && value.elapsed_ms >= 0
|
|
83
|
+
&& typeof value.sweep_ms === 'number' && Number.isFinite(value.sweep_ms) && value.sweep_ms >= 0
|
|
84
|
+
&& typeof value.overrun === 'boolean' && Array.isArray(value.values) && value.values.length <= 256) {
|
|
85
|
+
const values = value.values.map((item) => {
|
|
86
|
+
if (!item || typeof item !== 'object' || !('tag' in item) || typeof item.tag !== 'string' || !('value' in item)
|
|
87
|
+
|| !(item.value === null || typeof item.value === 'boolean' || typeof item.value === 'number' && Number.isFinite(item.value))
|
|
88
|
+
|| !('quality' in item) || !['good', 'bad'].includes(String(item.quality))
|
|
89
|
+
|| !('observed_at' in item) || typeof item.observed_at !== 'string' || !Number.isFinite(Date.parse(item.observed_at)))
|
|
90
|
+
throw new Error('Invalid polling value');
|
|
91
|
+
return { tag: item.tag, value: item.value, quality: String(item.quality), observed_at: item.observed_at,
|
|
92
|
+
...('error' in item && typeof item.error === 'string' ? { error: item.error.slice(0, 1000) } : {}) };
|
|
93
|
+
});
|
|
94
|
+
return { type: 'sample', index: value.index, elapsed_ms: value.elapsed_ms, sweep_ms: value.sweep_ms, overrun: value.overrun, values };
|
|
95
|
+
}
|
|
96
|
+
throw new Error('Invalid polling frame');
|
|
97
|
+
}
|
|
98
|
+
/** Own isolated connections across chats and await worker exit during disposal. */
|
|
99
|
+
export class DirectPolling {
|
|
100
|
+
workerPath;
|
|
101
|
+
config;
|
|
102
|
+
active = new Map();
|
|
103
|
+
disposed = false;
|
|
104
|
+
constructor(workerPath, config) {
|
|
105
|
+
this.workerPath = workerPath;
|
|
106
|
+
this.config = config;
|
|
107
|
+
if (config.minIntervalMs > config.defaultIntervalMs || config.defaultIntervalMs > config.maxDurationMs
|
|
108
|
+
|| config.allowedAddresses.some(address => !isIPv4(address)))
|
|
109
|
+
throw new Error('Invalid direct polling interval or IPv4 allowlist');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Start one bounded read window; repeated sweeps reuse its connection.
|
|
113
|
+
* @param request - Validated request.
|
|
114
|
+
* @param onFrame - Synchronous logged-output consumer.
|
|
115
|
+
* @param signal - Optional foreground lifetime.
|
|
116
|
+
* @returns Owned cancellation and quiescent completion.
|
|
117
|
+
*/
|
|
118
|
+
start(request, onFrame, signal) {
|
|
119
|
+
const key = `${request.ip}:${request.slot}`;
|
|
120
|
+
if (this.disposed || this.active.has(key) || this.active.size >= this.config.maxConcurrent)
|
|
121
|
+
throw new EngineeringError('POLL_BUSY', 'Controller polling capacity is occupied or the plugin is stopping');
|
|
122
|
+
signal?.throwIfAborted();
|
|
123
|
+
const worker = new Worker(this.workerPath, { workerData: request, env: {}, stdout: true, stderr: true });
|
|
124
|
+
// Only structured frames enter tool/job output; drain private driver diagnostics.
|
|
125
|
+
worker.stdout.resume();
|
|
126
|
+
worker.stderr.resume();
|
|
127
|
+
const frames = [];
|
|
128
|
+
let identified = false;
|
|
129
|
+
let sampleIndex = 0;
|
|
130
|
+
let lastElapsed = -1;
|
|
131
|
+
let failure;
|
|
132
|
+
const cancel = () => { failure ??= new EngineeringError('POLL_CANCELLED', 'Polling was cancelled'); void worker.terminate(); };
|
|
133
|
+
const timer = setTimeout(() => { failure = new EngineeringError('POLL_TIMEOUT', 'Polling exceeded its configured deadline'); void worker.terminate(); }, this.config.maxDurationMs + this.config.requestTimeoutMs);
|
|
134
|
+
const connecting = setTimeout(() => { failure = new EngineeringError('POLL_TIMEOUT', 'Controller connection exceeded its configured deadline'); void worker.terminate(); }, this.config.requestTimeoutMs);
|
|
135
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
136
|
+
const done = new Promise((resolve, reject) => {
|
|
137
|
+
worker.on('message', (message) => {
|
|
138
|
+
if (failure)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
const frame = decodePollFrame(message);
|
|
142
|
+
if (frame.type === 'identity') {
|
|
143
|
+
if (identified || frames.length !== 0)
|
|
144
|
+
throw new Error('Unexpected controller identity frame');
|
|
145
|
+
identified = true;
|
|
146
|
+
clearTimeout(connecting);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (!identified || frame.index !== sampleIndex || frame.elapsed_ms < lastElapsed || frame.values.length !== request.tags.length
|
|
150
|
+
|| frame.values.some((value, index) => value.tag !== (request.tags[index].program ? `Program:${request.tags[index].program}.${request.tags[index].name}` : request.tags[index].name)))
|
|
151
|
+
throw new Error('Polling samples do not match the requested watch');
|
|
152
|
+
sampleIndex++;
|
|
153
|
+
lastElapsed = frame.elapsed_ms;
|
|
154
|
+
}
|
|
155
|
+
if (frames.length >= request.samples + 1)
|
|
156
|
+
throw new Error('Polling frame limit exceeded');
|
|
157
|
+
frames.push(frame);
|
|
158
|
+
onFrame(frame);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
failure = error instanceof Error ? error : new Error('Polling frame failed');
|
|
162
|
+
void worker.terminate();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
worker.once('error', error => { failure = error; });
|
|
166
|
+
worker.once('exit', code => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
clearTimeout(connecting);
|
|
169
|
+
signal?.removeEventListener('abort', cancel);
|
|
170
|
+
this.active.delete(key);
|
|
171
|
+
if (failure)
|
|
172
|
+
reject(failure);
|
|
173
|
+
else if (code !== 0 || frames.filter(frame => frame.type === 'sample').length !== request.samples)
|
|
174
|
+
reject(new Error('Polling ended without all requested samples'));
|
|
175
|
+
else
|
|
176
|
+
resolve(frames);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
const handle = { cancel, done };
|
|
180
|
+
this.active.set(key, handle);
|
|
181
|
+
// A just-returned background handle may be collected on a later turn.
|
|
182
|
+
void done.catch(() => { });
|
|
183
|
+
return handle;
|
|
184
|
+
}
|
|
185
|
+
/** Stop only owned workers and await all exits. @returns Quiescent disposal. */
|
|
186
|
+
async dispose() {
|
|
187
|
+
this.disposed = true;
|
|
188
|
+
const handles = [...this.active.values()];
|
|
189
|
+
for (const handle of handles)
|
|
190
|
+
handle.cancel();
|
|
191
|
+
await Promise.allSettled(handles.map(handle => handle.done));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Change } from './review.ts';
|
|
2
|
+
import { type ProjectSnapshot } from '../session.ts';
|
|
3
|
+
export interface DownloadReview {
|
|
4
|
+
readonly baseline_sha256: string;
|
|
5
|
+
readonly source_sha256: string;
|
|
6
|
+
readonly controller: string;
|
|
7
|
+
readonly processor_type: string;
|
|
8
|
+
readonly change_count: number;
|
|
9
|
+
readonly changes: readonly Change[];
|
|
10
|
+
readonly safety_changed: boolean;
|
|
11
|
+
readonly eligible: boolean;
|
|
12
|
+
readonly blockers: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
/** This review is a release gate, not a claim that the machine is safe to run. */
|
|
15
|
+
export declare function reviewDownloadSource(baseline: ProjectSnapshot, source: ProjectSnapshot): DownloadReview;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { compareProjects } from "./review.js";
|
|
2
|
+
import { EngineeringError } from "../session.js";
|
|
3
|
+
function safetyPrograms(project) {
|
|
4
|
+
const names = new Set(project.document.programs.filter(program => /safety/iu.test(program.name)).map(program => program.name));
|
|
5
|
+
for (const task of project.document.tasks) {
|
|
6
|
+
if (/safety/iu.test(task.name))
|
|
7
|
+
for (const name of task.programs)
|
|
8
|
+
names.add(name);
|
|
9
|
+
}
|
|
10
|
+
for (const match of project.xml.matchAll(/<Program\b([^>]*)>/gu)) {
|
|
11
|
+
if (!/\bClass=["']Safety["']/iu.test(match[1] ?? ''))
|
|
12
|
+
continue;
|
|
13
|
+
const name = /\bName=["']([^"']+)["']/u.exec(match[1] ?? '')?.[1];
|
|
14
|
+
if (name !== undefined)
|
|
15
|
+
names.add(name);
|
|
16
|
+
}
|
|
17
|
+
return [...names].sort();
|
|
18
|
+
}
|
|
19
|
+
function safetyState(project) {
|
|
20
|
+
const programs = safetyPrograms(project);
|
|
21
|
+
const safetyInfo = /<SafetyInfo\b[\s\S]*?(?:\/>|<\/SafetyInfo>)/u.exec(project.xml)?.[0] ?? '';
|
|
22
|
+
const safetyTags = [...project.xml.matchAll(/<Tag\b[^>]*(?:\bClass=["']Safety["']|\bSafety=["']true["'])[^>]*(?:\/>|>[\s\S]*?<\/Tag>)/giu)].map(match => match[0]);
|
|
23
|
+
const safetyMappings = [...project.xml.matchAll(/<SafetyTagMap(?:ping)?\b[\s\S]*?(?:\/>|<\/SafetyTagMap(?:ping)?>)/giu)].map(match => match[0]);
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
safetyInfo,
|
|
26
|
+
safetyTags,
|
|
27
|
+
safetyMappings,
|
|
28
|
+
programs,
|
|
29
|
+
tasks: project.document.tasks.filter(task => /safety/iu.test(task.name) || task.programs.some(name => programs.includes(name))),
|
|
30
|
+
definitions: project.document.programs.filter(program => programs.includes(program.name)),
|
|
31
|
+
routines: project.document.routines.filter(routine => programs.includes(routine.program)),
|
|
32
|
+
tags: project.document.tags.filter(tag => programs.includes(tag.scope)),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function encodedDefinitions(project) {
|
|
36
|
+
return [...project.xml.matchAll(/<EncodedData\b[\s\S]*?<\/EncodedData>|<EncodedData\b[^>]*\/>/gu)].map(match => match[0]);
|
|
37
|
+
}
|
|
38
|
+
/** This review is a release gate, not a claim that the machine is safe to run. */
|
|
39
|
+
export function reviewDownloadSource(baseline, source) {
|
|
40
|
+
if (baseline.exportKind !== 'controller' || source.exportKind !== 'controller')
|
|
41
|
+
throw new EngineeringError('CONTROLLER_EXPORT_REQUIRED', 'Both baseline and source must be full controller exports');
|
|
42
|
+
const differences = compareProjects(baseline, source);
|
|
43
|
+
const safetyChanged = safetyState(baseline) !== safetyState(source);
|
|
44
|
+
const blockers = [];
|
|
45
|
+
if (baseline.document.controller.name !== source.document.controller.name || baseline.document.controller.processorType !== source.document.controller.processorType)
|
|
46
|
+
blockers.push('Controller identity or processor type differs from baseline');
|
|
47
|
+
if (baseline.softwareRevision !== source.softwareRevision)
|
|
48
|
+
blockers.push('Studio project revision differs from baseline');
|
|
49
|
+
if (differences.changes.length === 0)
|
|
50
|
+
blockers.push('No engineering change was found relative to baseline; refusing an unchanged backup download');
|
|
51
|
+
if (safetyChanged)
|
|
52
|
+
blockers.push('Safety task, program, routine, tag, or safety metadata differs; use Studio 5000 safety review');
|
|
53
|
+
if (differences.changes.some(change => change.component === 'module'))
|
|
54
|
+
blockers.push('I/O module configuration differs from baseline; review in Studio 5000');
|
|
55
|
+
if (JSON.stringify(encodedDefinitions(baseline)) !== JSON.stringify(encodedDefinitions(source)))
|
|
56
|
+
blockers.push('Encoded or source-protected definition changed');
|
|
57
|
+
return {
|
|
58
|
+
baseline_sha256: baseline.sha256, source_sha256: source.sha256,
|
|
59
|
+
controller: source.document.controller.name, processor_type: source.document.controller.processorType,
|
|
60
|
+
change_count: differences.changes.length, changes: differences.changes,
|
|
61
|
+
safety_changed: safetyChanged, eligible: blockers.length === 0, blockers,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Source-positioned Logix tokens and NeutralText structure for static analysis. */
|
|
2
|
+
/** Offsets refer to UTF-16 positions in the original source, with an exclusive end. */
|
|
3
|
+
export interface LogicToken {
|
|
4
|
+
readonly kind: 'identifier' | 'number' | 'string' | 'punctuation';
|
|
5
|
+
readonly text: string;
|
|
6
|
+
readonly start: number;
|
|
7
|
+
readonly end: number;
|
|
8
|
+
readonly line: number;
|
|
9
|
+
}
|
|
10
|
+
/** An unsupported or malformed source region; partial analysis retains its location. */
|
|
11
|
+
export interface SyntaxIssue {
|
|
12
|
+
readonly offset: number;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
}
|
|
15
|
+
/** A ladder instruction or parallel branch retains its exact source extent. */
|
|
16
|
+
export type LadderNode = {
|
|
17
|
+
readonly kind: 'instruction';
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly operands: readonly (readonly LogicToken[])[];
|
|
20
|
+
readonly start: number;
|
|
21
|
+
readonly end: number;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'branch';
|
|
24
|
+
readonly paths: readonly (readonly LadderNode[])[];
|
|
25
|
+
readonly start: number;
|
|
26
|
+
readonly end: number;
|
|
27
|
+
};
|
|
28
|
+
/** Tokenize Logix source without treating comments or quoted literals as symbols.
|
|
29
|
+
* @param source - Original routine or rung text.
|
|
30
|
+
* @returns Source tokens and lexical issues; newlines in comments and literals retain their positions.
|
|
31
|
+
*/
|
|
32
|
+
export declare function lexLogic(source: string): {
|
|
33
|
+
tokens: readonly LogicToken[];
|
|
34
|
+
issues: readonly SyntaxIssue[];
|
|
35
|
+
};
|
|
36
|
+
/** Parse standard NeutralText instructions and nested parallel branches.
|
|
37
|
+
* @param source - One complete rung, including its terminator.
|
|
38
|
+
* @returns Parsed nodes and located issues; malformed structure is never silently complete.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseLadder(source: string): {
|
|
41
|
+
nodes: readonly LadderNode[];
|
|
42
|
+
issues: readonly SyntaxIssue[];
|
|
43
|
+
};
|
|
44
|
+
/** One address token sequence, including indices and its source occurrence. */
|
|
45
|
+
export interface AddressOccurrence {
|
|
46
|
+
readonly text: string;
|
|
47
|
+
readonly start: number;
|
|
48
|
+
readonly end: number;
|
|
49
|
+
readonly line: number;
|
|
50
|
+
readonly indexTokens: readonly LogicToken[];
|
|
51
|
+
}
|
|
52
|
+
/** Read an identifier with optional program qualification, member paths and indices.
|
|
53
|
+
* @param tokens - Source-positioned tokens.
|
|
54
|
+
* @param offset - Index of the address's first identifier.
|
|
55
|
+
* @returns The address and next token index, or undefined for malformed indices.
|
|
56
|
+
*/
|
|
57
|
+
export declare function addressAt(tokens: readonly LogicToken[], offset: number): {
|
|
58
|
+
address: AddressOccurrence;
|
|
59
|
+
next: number;
|
|
60
|
+
} | undefined;
|