@codesys/mcp-toolkit 1.5.8 → 2.0.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 +153 -189
- package/LICENSE +21 -21
- package/README.md +269 -152
- package/dist/actions.d.ts +28 -0
- package/dist/actions.js +98 -0
- package/dist/actions.js.map +1 -0
- package/dist/bridge/bridge.d.ts +105 -0
- package/dist/bridge/bridge.js +400 -0
- package/dist/bridge/bridge.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +196 -0
- package/dist/cli.js.map +1 -0
- package/dist/codesys/cache.d.ts +48 -0
- package/dist/codesys/cache.js +82 -0
- package/dist/codesys/cache.js.map +1 -0
- package/dist/codesys/changes.d.ts +57 -0
- package/dist/codesys/changes.js +133 -0
- package/dist/codesys/changes.js.map +1 -0
- package/dist/codesys/decl.d.ts +45 -0
- package/dist/codesys/decl.js +251 -0
- package/dist/codesys/decl.js.map +1 -0
- package/dist/codesys/devices.d.ts +34 -0
- package/dist/codesys/devices.js +82 -0
- package/dist/codesys/devices.js.map +1 -0
- package/dist/codesys/diff.d.ts +13 -0
- package/dist/codesys/diff.js +61 -0
- package/dist/codesys/diff.js.map +1 -0
- package/dist/codesys/install.d.ts +40 -0
- package/dist/codesys/install.js +144 -0
- package/dist/codesys/install.js.map +1 -0
- package/dist/codesys/library-browse.d.ts +19 -0
- package/dist/codesys/library-browse.js +57 -0
- package/dist/codesys/library-browse.js.map +1 -0
- package/dist/codesys/outline.d.ts +32 -0
- package/dist/codesys/outline.js +219 -0
- package/dist/codesys/outline.js.map +1 -0
- package/dist/codesys/simulation.d.ts +34 -0
- package/dist/codesys/simulation.js +57 -0
- package/dist/codesys/simulation.js.map +1 -0
- package/dist/codesys/st.d.ts +36 -0
- package/dist/codesys/st.js +69 -0
- package/dist/codesys/st.js.map +1 -0
- package/dist/daemon.d.ts +46 -0
- package/dist/daemon.js +173 -0
- package/dist/daemon.js.map +1 -0
- package/dist/driver.d.ts +25 -0
- package/dist/driver.js +33 -0
- package/dist/driver.js.map +1 -0
- package/dist/panel/sim-panel.html +357 -0
- package/dist/panel/viewer.html +126 -0
- package/dist/panel-types.d.ts +20 -0
- package/dist/panel-types.js +3 -0
- package/dist/panel-types.js.map +1 -0
- package/dist/panel.d.ts +27 -0
- package/dist/panel.js +208 -0
- package/dist/panel.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +33 -974
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +103 -0
- package/dist/tools.js +793 -0
- package/dist/tools.js.map +1 -0
- package/dist/viewer.d.ts +14 -0
- package/dist/viewer.js +125 -0
- package/dist/viewer.js.map +1 -0
- package/package.json +66 -51
- package/watcher/mcp_compat.py +147 -0
- package/watcher/mcp_ops.py +1178 -0
- package/watcher/mcp_sim.py +246 -0
- package/watcher/watcher.py +255 -0
- package/.claude/settings.local.json +0 -29
- package/CLAUDE.md +0 -26
- package/CONTRIBUTING.md +0 -72
- package/README.md.enhanced +0 -155
- package/codesys-mcp-toolkit-1.2.0.tgz +0 -0
- package/codesys-mcp-toolkit-1.2.1.tgz +0 -0
- package/codesys-mcp-toolkit-1.5.7.tgz +0 -0
- package/codesys-mcp-toolkit-1.5.8.tgz +0 -0
- package/dist/bin.js +0 -75
- package/dist/check_session_paths.js +0 -149
- package/dist/codesys_interop.js +0 -435
- package/dist/direct_session_command.js +0 -293
- package/dist/scriptLoader.js +0 -165
- package/dist/scripts/check_status.py +0 -21
- package/dist/scripts/compile_project.py +0 -65
- package/dist/scripts/create_method.py +0 -80
- package/dist/scripts/create_pou.py +0 -102
- package/dist/scripts/create_project.py +0 -116
- package/dist/scripts/create_property.py +0 -78
- package/dist/scripts/ensure_project_open.py +0 -154
- package/dist/scripts/find_object_by_path.py +0 -111
- package/dist/scripts/get_pou_code.py +0 -91
- package/dist/scripts/get_project_structure.py +0 -72
- package/dist/scripts/open_project.py +0 -25
- package/dist/scripts/save_project.py +0 -29
- package/dist/scripts/session_status.py +0 -281
- package/dist/scripts/set_pou_code.py +0 -109
- package/dist/scripts/start_session.py +0 -809
- package/dist/scripts/stop_session.py +0 -324
- package/dist/sessionManager.js +0 -798
- package/docs/configuration.md +0 -141
- package/docs/index.md +0 -26
- package/docs/installation.md +0 -84
- package/docs/session_management.md +0 -110
- package/examples/README.md +0 -78
- package/examples/sample_config.json +0 -32
- package/replacements_session_status.txt +0 -16
- package/scriptDoc.txt +0 -8679
- package/scripts/build.js +0 -25
- package/scripts/fix_fstrings.py +0 -76
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Operation handlers for the codesys-mcp-toolkit watcher.
|
|
3
|
+
# Runs inside CODESYS (IronPython 2.7) and under CPython 3 with a fake
|
|
4
|
+
# scriptengine in tests. Keep this file ASCII-only.
|
|
5
|
+
from __future__ import print_function
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import scriptengine as se
|
|
12
|
+
|
|
13
|
+
from mcp_compat import PY2, to_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OpError(Exception):
|
|
17
|
+
def __init__(self, code, message):
|
|
18
|
+
Exception.__init__(self, message)
|
|
19
|
+
self.code = code
|
|
20
|
+
self.message = message
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Object type GUIDs (verified against CODESYS V3.5 SP21). Names are
|
|
24
|
+
# localised by CODESYS ("Taskkonfiguration"), so kinds come from GUIDs only.
|
|
25
|
+
GUID_KIND = {
|
|
26
|
+
"6f9dac99-8de1-4efc-8465-68ac443b7d08": "pou",
|
|
27
|
+
"f8a58466-d7f6-439f-bbb8-d4600e41d099": "method",
|
|
28
|
+
"5a3b8626-d3e9-4f37-98b5-66420063d91e": "property",
|
|
29
|
+
"792f2eb6-721e-4e64-ba20-bc98351056db": "accessor",
|
|
30
|
+
"8ac092e5-3128-4e26-9e7e-11016c6684f2": "action",
|
|
31
|
+
"2db5746d-d284-4425-9f7f-2663a34b0ebc": "dut",
|
|
32
|
+
"ffbfa93a-b94d-45fc-a329-229860183b1d": "gvl",
|
|
33
|
+
"6654496c-404d-479a-aad2-8551054e5f1e": "interface",
|
|
34
|
+
"f89f7675-27f1-46b3-8abb-b7da8e774ffd": "interface_method",
|
|
35
|
+
"ae1de277-a207-4a28-9efb-456c06bd52f3": "task_config",
|
|
36
|
+
"98a2708a-9b18-4f31-82ed-a1465b24fa2d": "task",
|
|
37
|
+
"413e2a7d-adb1-4d2c-be29-6ae6e4fab820": "task_call",
|
|
38
|
+
"adb5cb65-8e1d-4a00-b70a-375ea27582f3": "library_manager",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
SCRIPT_MESSAGE_CATEGORY = "194b48a9-ab51-43ae-b9a9-51d3edaaddf3"
|
|
42
|
+
|
|
43
|
+
_LEAD = r"^\s*(?:(?:\{[^}]*\}|//[^\n]*\n|\(\*.*?\*\))\s*)*"
|
|
44
|
+
_HEADER_RE = re.compile(_LEAD + r"(PROGRAM|FUNCTION_BLOCK|FUNCTION|METHOD|PROPERTY|INTERFACE|TYPE|VAR_GLOBAL)\b", re.S | re.I)
|
|
45
|
+
_DUT_RE = re.compile(_LEAD + r"TYPE\s+\w+\s*(?:EXTENDS\s+[\w.]+\s*)?:\s*(STRUCT|UNION|\()?", re.S | re.I)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------- helpers
|
|
49
|
+
|
|
50
|
+
def _safe(fn, default=None):
|
|
51
|
+
try:
|
|
52
|
+
return fn()
|
|
53
|
+
except Exception:
|
|
54
|
+
return default
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def guid_of(obj):
|
|
58
|
+
return (to_text(_safe(lambda: obj.guid)) or "").strip("{}").lower()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def name_of(obj):
|
|
62
|
+
return to_text(obj.get_name())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def children(obj):
|
|
66
|
+
return list(obj.get_children(False))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def decl_of(obj):
|
|
70
|
+
if _safe(lambda: obj.has_textual_declaration, False):
|
|
71
|
+
return to_text(obj.textual_declaration.text)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def impl_of(obj):
|
|
76
|
+
if _safe(lambda: obj.has_textual_implementation, False):
|
|
77
|
+
return to_text(obj.textual_implementation.text)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def header_keyword(decl):
|
|
82
|
+
m = _HEADER_RE.match(decl or "")
|
|
83
|
+
return m.group(1).upper() if m else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def subkind(kind, decl):
|
|
87
|
+
if kind == "pou":
|
|
88
|
+
kw = header_keyword(decl)
|
|
89
|
+
return {"PROGRAM": "program", "FUNCTION_BLOCK": "function_block", "FUNCTION": "function"}.get(kw)
|
|
90
|
+
if kind == "dut":
|
|
91
|
+
m = _DUT_RE.match(decl or "")
|
|
92
|
+
if not m:
|
|
93
|
+
return None
|
|
94
|
+
tok = (m.group(1) or "").upper()
|
|
95
|
+
return {"STRUCT": "struct", "UNION": "union", "(": "enum"}.get(tok, "alias")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def kind_of(obj, app_guid=None):
|
|
100
|
+
k = GUID_KIND.get((to_text(_safe(lambda: obj.type)) or "").strip("{}").lower())
|
|
101
|
+
if k:
|
|
102
|
+
return k
|
|
103
|
+
if app_guid and guid_of(obj) == app_guid:
|
|
104
|
+
return "application"
|
|
105
|
+
if _safe(lambda: obj.is_folder, False):
|
|
106
|
+
return "folder"
|
|
107
|
+
if _safe(lambda: obj.is_device, False):
|
|
108
|
+
return "device"
|
|
109
|
+
return "other"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_st(obj, kind):
|
|
113
|
+
"""True for text objects, False for graphical bodies (SFC/CFC/LD/FBD/IL)."""
|
|
114
|
+
if kind in ("pou", "method", "action", "accessor"):
|
|
115
|
+
return bool(_safe(lambda: obj.has_textual_implementation, False))
|
|
116
|
+
if kind in ("dut", "gvl", "interface", "interface_method", "property"):
|
|
117
|
+
return True
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def chain_names(obj):
|
|
122
|
+
"""Names from the top of the project down to obj."""
|
|
123
|
+
names = []
|
|
124
|
+
cur = obj
|
|
125
|
+
for _ in range(64):
|
|
126
|
+
if cur is None:
|
|
127
|
+
break
|
|
128
|
+
nm = _safe(lambda: cur.get_name())
|
|
129
|
+
if nm is None:
|
|
130
|
+
break
|
|
131
|
+
names.insert(0, to_text(nm))
|
|
132
|
+
cur = _safe(lambda: cur.parent)
|
|
133
|
+
return names
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Ctx(object):
|
|
137
|
+
def __init__(self, require_project=True):
|
|
138
|
+
self.proj = se.projects.primary
|
|
139
|
+
if self.proj is None and require_project:
|
|
140
|
+
raise OpError("no_project", "No project is open. Call open_project first.")
|
|
141
|
+
self.app = _safe(lambda: self.proj.active_application) if self.proj is not None else None
|
|
142
|
+
self.app_guid = guid_of(self.app) if self.app is not None else None
|
|
143
|
+
self.app_chain = chain_names(self.app) if self.app is not None else []
|
|
144
|
+
|
|
145
|
+
def rel(self, names):
|
|
146
|
+
n = len(self.app_chain)
|
|
147
|
+
if n and names[:n] == self.app_chain and len(names) > n:
|
|
148
|
+
return "/".join(names[n:])
|
|
149
|
+
return "/" + "/".join(names)
|
|
150
|
+
|
|
151
|
+
def resolve(self, path):
|
|
152
|
+
path = (path or "").strip()
|
|
153
|
+
if path in ("", ".", "/"):
|
|
154
|
+
if path == "/" or self.app is None:
|
|
155
|
+
raise OpError("bad_path", "Give an object path, e.g. 'PLC_PRG' or 'FB_Pump/Start'.")
|
|
156
|
+
return self.app
|
|
157
|
+
absolute = path.startswith("/")
|
|
158
|
+
parts = [p for p in path.split("/") if p]
|
|
159
|
+
if absolute or self.app is None:
|
|
160
|
+
base_children = children(self.proj)
|
|
161
|
+
cur = None
|
|
162
|
+
else:
|
|
163
|
+
if parts and parts[0].lower() == name_of(self.app).lower() and _find(children(self.app), parts[0]) is None:
|
|
164
|
+
parts = parts[1:]
|
|
165
|
+
base_children = None
|
|
166
|
+
cur = self.app
|
|
167
|
+
walked = []
|
|
168
|
+
for part in parts:
|
|
169
|
+
pool = base_children if cur is None else children(cur)
|
|
170
|
+
nxt = _find(pool, part)
|
|
171
|
+
if nxt is None:
|
|
172
|
+
where = "/".join(walked) or ("project root" if (absolute or self.app is None) else name_of(self.app))
|
|
173
|
+
hint = ", ".join(sorted(name_of(c) for c in pool)[:25])
|
|
174
|
+
raise OpError("not_found", "No object '%s' under %s. Children there: %s" % (part, where, hint or "(none)"))
|
|
175
|
+
walked.append(name_of(nxt))
|
|
176
|
+
cur = nxt
|
|
177
|
+
return cur
|
|
178
|
+
|
|
179
|
+
def path_of(self, obj):
|
|
180
|
+
return self.rel(chain_names(obj))
|
|
181
|
+
|
|
182
|
+
def save_if(self, args):
|
|
183
|
+
if args.get("save"):
|
|
184
|
+
self.proj.save()
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _find(objs, name):
|
|
190
|
+
for c in objs:
|
|
191
|
+
if name_of(c) == name:
|
|
192
|
+
return c
|
|
193
|
+
low = name.lower()
|
|
194
|
+
for c in objs:
|
|
195
|
+
if name_of(c).lower() == low:
|
|
196
|
+
return c
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _norm(p):
|
|
201
|
+
return os.path.normcase(os.path.abspath(p)) if p else p
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _build_props(obj):
|
|
205
|
+
"""An object's build properties, only those that are set (they are rare)."""
|
|
206
|
+
out = {}
|
|
207
|
+
if _safe(lambda: obj.effectively_excluded_from_build, False):
|
|
208
|
+
out["excluded"] = True
|
|
209
|
+
bp = _safe(lambda: obj.build_properties)
|
|
210
|
+
if bp is not None:
|
|
211
|
+
if _safe(lambda: bp.link_always_is_valid, False) and _safe(lambda: bp.link_always, False):
|
|
212
|
+
out["link_always"] = True
|
|
213
|
+
if _safe(lambda: bp.external_is_valid, False) and _safe(lambda: bp.external, False):
|
|
214
|
+
out["external"] = True
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _split_defines(text):
|
|
219
|
+
"""CODESYS compiler defines: 'A, B:=2; C' -> ['A', 'B', 'C']."""
|
|
220
|
+
out = []
|
|
221
|
+
for part in re.split(r"[,;\s]+", to_text(text) or ""):
|
|
222
|
+
name = part.split(":=")[0].strip()
|
|
223
|
+
if name:
|
|
224
|
+
out.append(name)
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _project_info(proj, ctx=None):
|
|
229
|
+
ctx = ctx or Ctx()
|
|
230
|
+
info = {
|
|
231
|
+
"path": to_text(_safe(lambda: proj.path)),
|
|
232
|
+
"dirty": bool(_safe(lambda: proj.dirty, False)),
|
|
233
|
+
"application": name_of(ctx.app) if ctx.app is not None else None,
|
|
234
|
+
}
|
|
235
|
+
if ctx.app is not None:
|
|
236
|
+
bp = _safe(lambda: ctx.app.build_properties)
|
|
237
|
+
if bp is not None and _safe(lambda: bp.compiler_defines_is_valid, False):
|
|
238
|
+
info["defines"] = _split_defines(_safe(lambda: bp.compiler_defines, ""))
|
|
239
|
+
settings = _safe(lambda: proj.project_settings)
|
|
240
|
+
if settings is not None:
|
|
241
|
+
# Scripting 4.2 and later
|
|
242
|
+
cv = _safe(lambda: settings.get_compilerversion())
|
|
243
|
+
if cv is None:
|
|
244
|
+
cv = _safe(lambda: settings.compilerversion)
|
|
245
|
+
if cv is not None:
|
|
246
|
+
info["compiler_version"] = to_text(cv)
|
|
247
|
+
pd = _safe(lambda: settings.project_defines)
|
|
248
|
+
if pd is not None:
|
|
249
|
+
info["project_defines"] = _split_defines(pd if isinstance(pd, (str, type(u""))) else ", ".join(to_text(x) for x in pd))
|
|
250
|
+
return info
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _node(ctx, obj, names, depth, with_text):
|
|
254
|
+
kind = kind_of(obj, ctx.app_guid)
|
|
255
|
+
decl = decl_of(obj) if (with_text or kind in ("pou", "dut")) else None
|
|
256
|
+
node = {
|
|
257
|
+
"path": ctx.rel(names),
|
|
258
|
+
"name": names[-1],
|
|
259
|
+
"kind": kind,
|
|
260
|
+
"depth": depth,
|
|
261
|
+
}
|
|
262
|
+
sk = subkind(kind, decl)
|
|
263
|
+
if sk:
|
|
264
|
+
node["sub"] = sk
|
|
265
|
+
st = is_st(obj, kind)
|
|
266
|
+
if st is not None:
|
|
267
|
+
node["st"] = st
|
|
268
|
+
if kind in ("pou", "gvl", "dut", "folder", "interface"):
|
|
269
|
+
b = _build_props(obj)
|
|
270
|
+
if b:
|
|
271
|
+
node["build"] = b
|
|
272
|
+
if with_text:
|
|
273
|
+
if decl is not None:
|
|
274
|
+
node["declaration"] = decl
|
|
275
|
+
impl = impl_of(obj)
|
|
276
|
+
if impl is not None:
|
|
277
|
+
node["implementation"] = impl
|
|
278
|
+
if st is False:
|
|
279
|
+
# A graphical body has no text: keep its PLCopenXML, so checkpoints can restore it.
|
|
280
|
+
xml = _export_xml(obj)
|
|
281
|
+
if xml:
|
|
282
|
+
node["xml"] = xml
|
|
283
|
+
return node
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _export_xml(obj):
|
|
287
|
+
import io
|
|
288
|
+
f = os.path.join(os.environ.get("CODESYS_MCP_DIR") or os.environ.get("TEMP") or ".", "node-export.xml")
|
|
289
|
+
try:
|
|
290
|
+
obj.export_xml(None, f, True)
|
|
291
|
+
with io.open(f, "r", encoding="utf-8-sig") as fh:
|
|
292
|
+
return fh.read()
|
|
293
|
+
except Exception:
|
|
294
|
+
return None
|
|
295
|
+
finally:
|
|
296
|
+
_safe(lambda: os.remove(f))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def op_object_import_xml(args):
|
|
300
|
+
"""Recreate an object from PLCopenXML (export_xml) under parent, e.g. a graphical POU
|
|
301
|
+
when a checkpoint is restored."""
|
|
302
|
+
import io
|
|
303
|
+
c = Ctx()
|
|
304
|
+
parent = c.resolve(args.get("parent") or ".")
|
|
305
|
+
before = set(name_of(x).lower() for x in children(parent))
|
|
306
|
+
f = os.path.join(os.environ.get("CODESYS_MCP_DIR") or os.environ.get("TEMP") or ".", "import.xml")
|
|
307
|
+
try:
|
|
308
|
+
with io.open(f, "w", encoding="utf-8") as fh:
|
|
309
|
+
fh.write(args["xml"] if not PY2 or isinstance(args["xml"], unicode) else args["xml"].decode("utf-8")) # noqa: F821
|
|
310
|
+
parent.import_xml(None, f)
|
|
311
|
+
finally:
|
|
312
|
+
_safe(lambda: os.remove(f))
|
|
313
|
+
added = [c.path_of(x) for x in children(parent) if name_of(x).lower() not in before]
|
|
314
|
+
if not added:
|
|
315
|
+
raise OpError("codesys_error", "CODESYS imported nothing under %s." % (args.get("parent") or "the application"))
|
|
316
|
+
return {"added": added, "saved": c.save_if(args)}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _walk(ctx, root, root_names, max_depth, with_text, out):
|
|
320
|
+
def rec(obj, names, depth):
|
|
321
|
+
for c in children(obj):
|
|
322
|
+
cn = names + [name_of(c)]
|
|
323
|
+
out.append(_node(ctx, c, cn, depth, with_text))
|
|
324
|
+
if max_depth is None or depth < max_depth:
|
|
325
|
+
rec(c, cn, depth + 1)
|
|
326
|
+
rec(root, root_names, 0)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------- ops
|
|
330
|
+
|
|
331
|
+
def op_ping(args):
|
|
332
|
+
info = {"engine": "codesys" if not hasattr(se, "FAKE") else "fake"}
|
|
333
|
+
proj = _safe(lambda: se.projects.primary)
|
|
334
|
+
info["project"] = to_text(_safe(lambda: proj.path)) if proj is not None else None
|
|
335
|
+
return info
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def op_project_open(args):
|
|
339
|
+
path = args["path"]
|
|
340
|
+
cur = se.projects.primary
|
|
341
|
+
if cur is not None:
|
|
342
|
+
if _norm(to_text(_safe(lambda: cur.path))) == _norm(path):
|
|
343
|
+
info = _project_info(cur)
|
|
344
|
+
info["reused"] = True
|
|
345
|
+
return info
|
|
346
|
+
if _safe(lambda: cur.dirty, False) and not args.get("discard_changes"):
|
|
347
|
+
raise OpError("unsaved_changes", "Project %s has unsaved changes. Save it, or pass discardChanges." % to_text(cur.path))
|
|
348
|
+
cur.close()
|
|
349
|
+
if not os.path.exists(path):
|
|
350
|
+
raise OpError("not_found", "Project file not found: %s" % path)
|
|
351
|
+
flags = None
|
|
352
|
+
try:
|
|
353
|
+
flags = se.VersionUpdateFlags.NoUpdates | se.VersionUpdateFlags.SilentMode
|
|
354
|
+
except AttributeError:
|
|
355
|
+
flags = None
|
|
356
|
+
if path.lower().endswith(".projectarchive"):
|
|
357
|
+
# Extract next to the archive (or where asked); never over an existing project.
|
|
358
|
+
target = args.get("extract_to") or (os.path.splitext(path)[0] + ".project")
|
|
359
|
+
if os.path.exists(target):
|
|
360
|
+
raise OpError("exists", "%s already exists. Open it directly, or pass extractTo for another file name." % target)
|
|
361
|
+
proj = (se.projects.open_archive(path, target, False, None, None, None, flags) if flags is not None
|
|
362
|
+
else se.projects.open_archive(path, target, False, None))
|
|
363
|
+
if proj is None:
|
|
364
|
+
raise OpError("open_failed", "CODESYS returned no project for archive %s" % path)
|
|
365
|
+
info = _project_info(proj)
|
|
366
|
+
info["extracted_from"] = path
|
|
367
|
+
return info
|
|
368
|
+
proj = se.projects.open(path, update_flags=flags) if flags is not None else se.projects.open(path)
|
|
369
|
+
if proj is None:
|
|
370
|
+
raise OpError("open_failed", "CODESYS returned no project for %s" % path)
|
|
371
|
+
return _project_info(proj)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _version_key(v):
|
|
375
|
+
return [int(x) if x.isdigit() else 0 for x in to_text(v).split(".")]
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _update_device_to_newest(dev):
|
|
379
|
+
"""Move a device to the newest version of it in the repository. Returns that version, or None."""
|
|
380
|
+
did = dev.get_device_identification()
|
|
381
|
+
same = [d.device_id for d in se.device_repository.get_all_devices()
|
|
382
|
+
if d.device_id.type == did.type and to_text(d.device_id.id) == to_text(did.id)]
|
|
383
|
+
if not same:
|
|
384
|
+
return None
|
|
385
|
+
newest = max(same, key=lambda x: _version_key(x.version))
|
|
386
|
+
if _version_key(newest.version) <= _version_key(did.version):
|
|
387
|
+
return None
|
|
388
|
+
dev.update(newest)
|
|
389
|
+
return to_text(newest.version)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def op_project_open_new(args):
|
|
393
|
+
"""Open a project just copied from CODESYS's Standard.project and bring it up to this
|
|
394
|
+
CODESYS. The shipped file can be years old: SP22's names device 3.1.3.0 and IoStandard
|
|
395
|
+
3.1.3.1, which a current install doesn't have, so it can't compile. Only for such a
|
|
396
|
+
fresh copy: a project the user opens is never updated (see op_project_open)."""
|
|
397
|
+
path = args["path"]
|
|
398
|
+
cur = se.projects.primary
|
|
399
|
+
if cur is not None:
|
|
400
|
+
if _safe(lambda: cur.dirty, False) and not args.get("discard_changes"):
|
|
401
|
+
raise OpError("unsaved_changes", "Project %s has unsaved changes. Save it, or pass discardChanges." % to_text(cur.path))
|
|
402
|
+
cur.close()
|
|
403
|
+
flags = se.VersionUpdateFlags
|
|
404
|
+
proj = se.projects.open(path, update_flags=flags.UpdateAll | flags.SilentMode)
|
|
405
|
+
if proj is None:
|
|
406
|
+
raise OpError("open_failed", "CODESYS returned no project for %s" % path)
|
|
407
|
+
updated = None
|
|
408
|
+
for obj in proj.get_children(False):
|
|
409
|
+
if _safe(lambda: obj.is_device, False):
|
|
410
|
+
updated = _update_device_to_newest(obj) or updated
|
|
411
|
+
proj.save()
|
|
412
|
+
# CODESYS keeps reporting the old template's load errors until the project is loaded again.
|
|
413
|
+
proj.close()
|
|
414
|
+
proj = se.projects.open(path, update_flags=flags.NoUpdates | flags.SilentMode)
|
|
415
|
+
if proj is None:
|
|
416
|
+
raise OpError("open_failed", "CODESYS returned no project for %s" % path)
|
|
417
|
+
info = _project_info(proj)
|
|
418
|
+
if updated:
|
|
419
|
+
info["updated_device"] = updated
|
|
420
|
+
return info
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def op_project_create_empty(args):
|
|
424
|
+
path = args["path"]
|
|
425
|
+
if os.path.exists(path):
|
|
426
|
+
raise OpError("exists", "File already exists: %s" % path)
|
|
427
|
+
cur = se.projects.primary
|
|
428
|
+
if cur is not None:
|
|
429
|
+
if _safe(lambda: cur.dirty, False) and not args.get("discard_changes"):
|
|
430
|
+
raise OpError("unsaved_changes", "Project %s has unsaved changes." % to_text(cur.path))
|
|
431
|
+
cur.close()
|
|
432
|
+
proj = se.projects.create(path, True)
|
|
433
|
+
proj.save()
|
|
434
|
+
return _project_info(proj, Ctx())
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def op_project_info(args):
|
|
438
|
+
c = Ctx(require_project=False)
|
|
439
|
+
if c.proj is None:
|
|
440
|
+
return {"open": False}
|
|
441
|
+
info = _project_info(c.proj, c)
|
|
442
|
+
info["open"] = True
|
|
443
|
+
return info
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def op_project_save(args):
|
|
447
|
+
c = Ctx()
|
|
448
|
+
c.proj.save()
|
|
449
|
+
info = _project_info(c.proj, c)
|
|
450
|
+
if args.get("archive"):
|
|
451
|
+
if os.path.exists(args["archive"]):
|
|
452
|
+
raise OpError("exists", "%s already exists." % args["archive"])
|
|
453
|
+
c.proj.save_archive(args["archive"])
|
|
454
|
+
info["archive"] = args["archive"]
|
|
455
|
+
return info
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def op_project_close(args):
|
|
459
|
+
c = Ctx(require_project=False)
|
|
460
|
+
if c.proj is None:
|
|
461
|
+
return {"closed": False}
|
|
462
|
+
if _safe(lambda: c.proj.dirty, False) and not args.get("discard_changes"):
|
|
463
|
+
raise OpError("unsaved_changes", "Project has unsaved changes. Save it, or pass discardChanges.")
|
|
464
|
+
c.proj.close()
|
|
465
|
+
return {"closed": True}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def op_tree(args):
|
|
469
|
+
c = Ctx()
|
|
470
|
+
out = []
|
|
471
|
+
if args.get("root"):
|
|
472
|
+
root = c.resolve(args["root"])
|
|
473
|
+
names = chain_names(root)
|
|
474
|
+
elif args.get("whole_project") or c.app is None:
|
|
475
|
+
root, names = c.proj, []
|
|
476
|
+
else:
|
|
477
|
+
root, names = c.app, c.app_chain
|
|
478
|
+
_walk(c, root, names, args.get("max_depth"), False, out)
|
|
479
|
+
return {"nodes": out, "application": name_of(c.app) if c.app is not None else None}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def op_objects_read(args):
|
|
483
|
+
c = Ctx()
|
|
484
|
+
res = []
|
|
485
|
+
for p in args.get("paths", []):
|
|
486
|
+
try:
|
|
487
|
+
obj = c.resolve(p)
|
|
488
|
+
res.append(_node(c, obj, chain_names(obj), 0, True))
|
|
489
|
+
except OpError as e:
|
|
490
|
+
res.append({"path": p, "error": e.message, "code": e.code})
|
|
491
|
+
return {"objects": res}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def op_object_write(args):
|
|
495
|
+
c = Ctx()
|
|
496
|
+
obj = c.resolve(args["path"])
|
|
497
|
+
kind = kind_of(obj, c.app_guid)
|
|
498
|
+
if "declaration" in args and args["declaration"] is not None:
|
|
499
|
+
if not _safe(lambda: obj.has_textual_declaration, False):
|
|
500
|
+
raise OpError("no_declaration", "%s (%s) has no textual declaration." % (args["path"], kind))
|
|
501
|
+
obj.textual_declaration.replace(args["declaration"])
|
|
502
|
+
if "implementation" in args and args["implementation"] is not None:
|
|
503
|
+
if not _safe(lambda: obj.has_textual_implementation, False):
|
|
504
|
+
raise OpError("not_st", "%s (%s) has no textual (ST) implementation; v2 edits ST only." % (args["path"], kind))
|
|
505
|
+
obj.textual_implementation.replace(args["implementation"])
|
|
506
|
+
return {"path": c.path_of(obj), "kind": kind, "saved": c.save_if(args)}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def op_object_create(args):
|
|
510
|
+
c = Ctx()
|
|
511
|
+
parent = c.resolve(args.get("parent") or ".")
|
|
512
|
+
kind = args["kind"]
|
|
513
|
+
name = args["name"]
|
|
514
|
+
rt = args.get("return_type")
|
|
515
|
+
if kind in ("program", "function_block", "function"):
|
|
516
|
+
pt = {"program": se.PouType.Program, "function_block": se.PouType.FunctionBlock,
|
|
517
|
+
"function": se.PouType.Function}[kind]
|
|
518
|
+
if kind == "function":
|
|
519
|
+
if not rt:
|
|
520
|
+
raise OpError("bad_args", "A function needs returnType.")
|
|
521
|
+
obj = parent.create_pou(name, pt, return_type=rt)
|
|
522
|
+
else:
|
|
523
|
+
obj = parent.create_pou(name, pt)
|
|
524
|
+
elif kind == "method":
|
|
525
|
+
obj = parent.create_method(name, rt) if rt else parent.create_method(name)
|
|
526
|
+
elif kind == "property":
|
|
527
|
+
if not rt:
|
|
528
|
+
raise OpError("bad_args", "A property needs returnType.")
|
|
529
|
+
obj = parent.create_property(name, rt)
|
|
530
|
+
elif kind == "action":
|
|
531
|
+
obj = parent.create_action(name)
|
|
532
|
+
elif kind == "interface":
|
|
533
|
+
base = args.get("base_interfaces")
|
|
534
|
+
try:
|
|
535
|
+
# Empty base list: no implicit __System.IQueryInterface.
|
|
536
|
+
obj = parent.create_interface(name, base if base else "")
|
|
537
|
+
except Exception:
|
|
538
|
+
obj = parent.create_interface(name)
|
|
539
|
+
elif kind == "dut":
|
|
540
|
+
dt = {"struct": se.DutType.Structure, "enum": se.DutType.Enumeration,
|
|
541
|
+
"union": se.DutType.Union, "alias": se.DutType.Alias}[args.get("dut_type") or "struct"]
|
|
542
|
+
obj = parent.create_dut(name, dt)
|
|
543
|
+
elif kind == "gvl":
|
|
544
|
+
obj = parent.create_gvl(name)
|
|
545
|
+
elif kind == "folder":
|
|
546
|
+
obj = parent.create_folder(name)
|
|
547
|
+
else:
|
|
548
|
+
raise OpError("bad_args", "Unknown kind %s" % kind)
|
|
549
|
+
if args.get("declaration") is not None:
|
|
550
|
+
obj.textual_declaration.replace(args["declaration"])
|
|
551
|
+
if args.get("implementation") is not None:
|
|
552
|
+
if not _safe(lambda: obj.has_textual_implementation, False):
|
|
553
|
+
raise OpError("not_st", "Created %s has no textual implementation." % name)
|
|
554
|
+
obj.textual_implementation.replace(args["implementation"])
|
|
555
|
+
return {"path": c.path_of(obj), "kind": kind_of(obj, c.app_guid), "saved": c.save_if(args)}
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def op_object_delete(args):
|
|
559
|
+
c = Ctx()
|
|
560
|
+
obj = c.resolve(args["path"])
|
|
561
|
+
path = c.path_of(obj)
|
|
562
|
+
obj.remove()
|
|
563
|
+
return {"path": path, "saved": c.save_if(args)}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def op_object_rename(args):
|
|
567
|
+
c = Ctx()
|
|
568
|
+
obj = c.resolve(args["path"])
|
|
569
|
+
obj.rename(args["new_name"])
|
|
570
|
+
return {"path": c.path_of(obj), "saved": c.save_if(args)}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def op_export(args):
|
|
574
|
+
c = Ctx()
|
|
575
|
+
out = []
|
|
576
|
+
if args.get("root"):
|
|
577
|
+
root = c.resolve(args["root"])
|
|
578
|
+
names = chain_names(root)
|
|
579
|
+
elif c.app is not None:
|
|
580
|
+
root, names = c.app, c.app_chain
|
|
581
|
+
else:
|
|
582
|
+
root, names = c.proj, []
|
|
583
|
+
_walk(c, root, names, None, True, out)
|
|
584
|
+
return {"nodes": out, "application": name_of(c.app) if c.app is not None else None}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def op_build(args):
|
|
588
|
+
c = Ctx()
|
|
589
|
+
if c.app is None:
|
|
590
|
+
raise OpError("no_application", "The project has no active application to build.")
|
|
591
|
+
steps = []
|
|
592
|
+
linked = []
|
|
593
|
+
was_dirty = bool(_safe(lambda: c.proj.dirty, True))
|
|
594
|
+
try:
|
|
595
|
+
if args.get("check_all"):
|
|
596
|
+
# A build only compiles what a task reaches. Mark every other POU, GVL and
|
|
597
|
+
# DUT "link always" for this build, then put the flags back.
|
|
598
|
+
# (project.check_all_pool_objects() checks the POU pool, not the
|
|
599
|
+
# application, and clears the build's messages.)
|
|
600
|
+
for o in _walk_all(c, c.app):
|
|
601
|
+
if kind_of(o, c.app_guid) not in ("pou", "gvl", "dut"):
|
|
602
|
+
continue
|
|
603
|
+
if _TEST_ATTR.search(decl_of(o) or ""):
|
|
604
|
+
continue # programs marked as tests can use assertions CODESYS doesn't know
|
|
605
|
+
bp = _safe(lambda: o.build_properties)
|
|
606
|
+
if bp is None or not _safe(lambda: bp.link_always_is_valid, False):
|
|
607
|
+
continue
|
|
608
|
+
if _safe(lambda: bp.exclude_from_build, False) or _safe(lambda: bp.link_always, True):
|
|
609
|
+
continue
|
|
610
|
+
bp.link_always = True
|
|
611
|
+
linked.append(bp)
|
|
612
|
+
steps.append("check_all")
|
|
613
|
+
if args.get("clean"):
|
|
614
|
+
c.app.clean()
|
|
615
|
+
steps.append("clean")
|
|
616
|
+
if args.get("deep"):
|
|
617
|
+
# Code generation finds errors the build alone doesn't.
|
|
618
|
+
c.app.generate_code()
|
|
619
|
+
steps.append("generate_code")
|
|
620
|
+
else:
|
|
621
|
+
c.app.build()
|
|
622
|
+
steps.append("build")
|
|
623
|
+
finally:
|
|
624
|
+
for bp in linked:
|
|
625
|
+
_safe(lambda: setattr(bp, "link_always", False))
|
|
626
|
+
if linked and not was_dirty:
|
|
627
|
+
_safe(lambda: c.proj.save()) # same content as before; leaves no unsaved change behind
|
|
628
|
+
msgs = []
|
|
629
|
+
seen = set()
|
|
630
|
+
for cat in se.system.get_message_categories(True):
|
|
631
|
+
if (to_text(cat) or "").strip("{}").lower() == SCRIPT_MESSAGE_CATEGORY:
|
|
632
|
+
continue
|
|
633
|
+
desc = to_text(_safe(lambda: se.system.get_message_category_description(cat), ""))
|
|
634
|
+
for m in se.system.get_message_objects(cat):
|
|
635
|
+
obj = _safe(lambda: m.object)
|
|
636
|
+
opath = c.path_of(obj) if obj is not None else None
|
|
637
|
+
key = (to_text(_safe(lambda: m.text, "")), to_text(_safe(lambda: m.position_text)), opath)
|
|
638
|
+
if key in seen: # the same message from the build and the full check
|
|
639
|
+
continue
|
|
640
|
+
seen.add(key)
|
|
641
|
+
msgs.append({
|
|
642
|
+
"category": desc,
|
|
643
|
+
"severity": to_text(_safe(lambda: m.severity, "")),
|
|
644
|
+
"text": to_text(_safe(lambda: m.text, "")),
|
|
645
|
+
"prefix": to_text(_safe(lambda: m.prefix)),
|
|
646
|
+
"number": _safe(lambda: int(m.number)),
|
|
647
|
+
"object": opath,
|
|
648
|
+
"position": _exact_position(m),
|
|
649
|
+
"length": _safe(lambda: int(m.length)),
|
|
650
|
+
})
|
|
651
|
+
return {"messages": msgs, "steps": steps}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _exact_position(m):
|
|
655
|
+
"""position_text, with the column from position_offset: CODESYS's text says
|
|
656
|
+
'Column 1' while position_offset holds where the token starts in the line
|
|
657
|
+
(verified on SP21: 'Line 2, Column 1' for an error at offset 4, length 6)."""
|
|
658
|
+
text = to_text(_safe(lambda: m.position_text))
|
|
659
|
+
offset = _safe(lambda: int(m.position_offset))
|
|
660
|
+
if text and offset is not None and offset >= 0:
|
|
661
|
+
text = re.sub(r"Column \d+", "Column %d" % (offset + 1), text, count=1)
|
|
662
|
+
return text
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _library_manager(c):
|
|
666
|
+
root = c.app if c.app is not None else c.proj
|
|
667
|
+
for ch in children(root):
|
|
668
|
+
if kind_of(ch, c.app_guid) == "library_manager":
|
|
669
|
+
return ch
|
|
670
|
+
raise OpError("no_library_manager", "No Library Manager found under %s." % (name_of(root) if c.app is not None else "the project"))
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def op_libraries_list(args):
|
|
674
|
+
c = Ctx()
|
|
675
|
+
lm = _library_manager(c)
|
|
676
|
+
refs = []
|
|
677
|
+
for r in list(_safe(lambda: lm.references, []) or []):
|
|
678
|
+
rec = {"name": to_text(_safe(lambda: r.name))}
|
|
679
|
+
for attr in ("is_placeholder", "is_managed", "namespace", "effective_resolution", "default_resolution"):
|
|
680
|
+
v = _safe(lambda: getattr(r, attr))
|
|
681
|
+
if v is not None:
|
|
682
|
+
rec[attr] = v if isinstance(v, bool) else to_text(v)
|
|
683
|
+
refs.append(rec)
|
|
684
|
+
if not refs:
|
|
685
|
+
refs = [{"name": to_text(n)} for n in (_safe(lambda: lm.get_libraries(False), []) or [])]
|
|
686
|
+
return {"libraries": refs}
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def op_library_api(args):
|
|
690
|
+
"""Declarations of a referenced library's objects, read from its source
|
|
691
|
+
.library file opened as a secondary, read-only project. Compiled libraries
|
|
692
|
+
(.compiled-library) carry no readable source."""
|
|
693
|
+
c = Ctx()
|
|
694
|
+
want = (args.get("name") or "").strip().lower()
|
|
695
|
+
lm = _library_manager(c)
|
|
696
|
+
match = None
|
|
697
|
+
for r in list(_safe(lambda: lm.references, []) or []):
|
|
698
|
+
nm = (to_text(_safe(lambda: r.name)) or "").strip()
|
|
699
|
+
short = nm.lstrip("#").split(",")[0].strip().lower()
|
|
700
|
+
if nm.lower() == want or short == want:
|
|
701
|
+
match = r
|
|
702
|
+
break
|
|
703
|
+
if match is None:
|
|
704
|
+
raise OpError("not_found", "No library reference named '%s'. Use list_libraries." % args.get("name"))
|
|
705
|
+
display = to_text(_safe(lambda: match.effective_resolution)) or to_text(_safe(lambda: match.name))
|
|
706
|
+
glm = getattr(se, "librarymanager", None)
|
|
707
|
+
if glm is None:
|
|
708
|
+
raise OpError("unavailable", "The CODESYS library manager is not available to scripts here.")
|
|
709
|
+
managed = _safe(lambda: match.managed_library)
|
|
710
|
+
if managed is None:
|
|
711
|
+
found = _safe(lambda: glm.find_library(display))
|
|
712
|
+
if found:
|
|
713
|
+
managed = found[0]
|
|
714
|
+
if managed is None:
|
|
715
|
+
raise OpError("not_installed", "Library %s is not installed in any repository." % display)
|
|
716
|
+
path = to_text(_safe(lambda: glm.get_file_path(managed))) or ""
|
|
717
|
+
info = {"library": display, "path": path, "title": to_text(_safe(lambda: managed.title)),
|
|
718
|
+
"version": to_text(_safe(lambda: managed.version)), "company": to_text(_safe(lambda: managed.company))}
|
|
719
|
+
if not path.lower().endswith(".library"):
|
|
720
|
+
info["compiled"] = True
|
|
721
|
+
info["nodes"] = []
|
|
722
|
+
return info
|
|
723
|
+
proj = se.projects.open(path, primary=False)
|
|
724
|
+
if proj is None:
|
|
725
|
+
raise OpError("open_failed", "CODESYS could not open %s." % path)
|
|
726
|
+
try:
|
|
727
|
+
out = []
|
|
728
|
+
_walk(c, proj, [], None, True, out)
|
|
729
|
+
for n in out:
|
|
730
|
+
n.pop("implementation", None)
|
|
731
|
+
info["nodes"] = out
|
|
732
|
+
finally:
|
|
733
|
+
_safe(lambda: proj.close())
|
|
734
|
+
return info
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _installed_libraries(name):
|
|
738
|
+
"""The installed libraries with this title; None when scripts can't see the repositories."""
|
|
739
|
+
glm = getattr(se, "librarymanager", None)
|
|
740
|
+
if glm is None:
|
|
741
|
+
return None
|
|
742
|
+
want = (name or "").strip().lstrip("#").split(",")[0].strip().lower()
|
|
743
|
+
return [l for l in glm.get_all_libraries() if (to_text(l.title) or "").lower() == want]
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def op_library_find(args):
|
|
747
|
+
found = _installed_libraries(args.get("name"))
|
|
748
|
+
if found is None:
|
|
749
|
+
raise OpError("unavailable", "The CODESYS library manager is not available to scripts here.")
|
|
750
|
+
return {
|
|
751
|
+
"repositories": [to_text(r.root_folder) for r in se.librarymanager.repositories],
|
|
752
|
+
"matches": [{"title": to_text(l.title), "version": to_text(l.version), "company": to_text(l.company)} for l in found],
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def op_libraries_add(args):
|
|
757
|
+
c = Ctx()
|
|
758
|
+
lm = _library_manager(c)
|
|
759
|
+
name = args["name"].strip()
|
|
760
|
+
if "," not in name:
|
|
761
|
+
# A bare title, e.g. "Util": the newest installed, or the version asked for.
|
|
762
|
+
found = _installed_libraries(name)
|
|
763
|
+
if found is not None:
|
|
764
|
+
if not found:
|
|
765
|
+
raise OpError("not_found", "No library titled '%s' is installed in the CODESYS repositories." % name)
|
|
766
|
+
newest = max(found, key=lambda l: _version_key(l.version))
|
|
767
|
+
name = "%s, %s (%s)" % (to_text(newest.title), args.get("version") or "*", to_text(newest.company))
|
|
768
|
+
lm.add_library(name)
|
|
769
|
+
return {"added": name, "saved": c.save_if(args)}
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def op_libraries_remove(args):
|
|
773
|
+
c = Ctx()
|
|
774
|
+
lm = _library_manager(c)
|
|
775
|
+
name = args["name"]
|
|
776
|
+
names = [to_text(n) for n in (_safe(lambda: list(lm.get_libraries(False)), []) or [])]
|
|
777
|
+
match = [n for n in names if n.lower() == name.lower()] or [n for n in names if n.lower().split(",")[0].strip() == name.lower().split(",")[0].strip()]
|
|
778
|
+
if not match:
|
|
779
|
+
raise OpError("not_found", "%s is not referenced. Referenced: %s" % (name, ", ".join(names) or "none"))
|
|
780
|
+
lm.remove_library(match[0])
|
|
781
|
+
left = [to_text(n) for n in (_safe(lambda: list(lm.get_libraries(False)), []) or [])]
|
|
782
|
+
if match[0] in left:
|
|
783
|
+
raise OpError("codesys_error", "CODESYS did not remove %s." % match[0])
|
|
784
|
+
return {"removed": match[0], "saved": c.save_if(args)}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def op_tasks_list(args):
|
|
788
|
+
c = Ctx()
|
|
789
|
+
tasks = []
|
|
790
|
+
root = c.app if c.app is not None else c.proj
|
|
791
|
+
|
|
792
|
+
def rec(obj):
|
|
793
|
+
for ch in children(obj):
|
|
794
|
+
k = kind_of(ch, c.app_guid)
|
|
795
|
+
if k == "task":
|
|
796
|
+
tasks.append(_task_info(c, ch))
|
|
797
|
+
elif k in ("task_config", "folder"):
|
|
798
|
+
rec(ch)
|
|
799
|
+
rec(root)
|
|
800
|
+
return {"tasks": tasks}
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def op_objects_xml(args):
|
|
804
|
+
"""Objects as PLCopenXML (export_xml), e.g. graphical bodies (SFC, CFC, LD, FBD) that
|
|
805
|
+
have no ST text. Read-only: the XML goes through a temp file in the session folder."""
|
|
806
|
+
import io
|
|
807
|
+
c = Ctx()
|
|
808
|
+
tmp = os.environ.get("CODESYS_MCP_DIR") or os.environ.get("TEMP") or "."
|
|
809
|
+
limit = int(args.get("max_chars") or 200000)
|
|
810
|
+
res = []
|
|
811
|
+
for k, p in enumerate(args.get("paths", [])):
|
|
812
|
+
f = os.path.join(tmp, "export-%d.xml" % k)
|
|
813
|
+
try:
|
|
814
|
+
obj = c.resolve(p)
|
|
815
|
+
obj.export_xml(None, f, True)
|
|
816
|
+
with io.open(f, "r", encoding="utf-8-sig") as fh:
|
|
817
|
+
xml = fh.read()
|
|
818
|
+
res.append({"path": c.path_of(obj), "kind": kind_of(obj, c.app_guid), "xml": xml[:limit], "truncated": len(xml) > limit, "chars": len(xml)})
|
|
819
|
+
except OpError as e:
|
|
820
|
+
res.append({"path": p, "error": e.message, "code": e.code})
|
|
821
|
+
except Exception as e:
|
|
822
|
+
res.append({"path": p, "error": to_text(e), "code": "codesys_error"})
|
|
823
|
+
finally:
|
|
824
|
+
_safe(lambda: os.remove(f))
|
|
825
|
+
return {"objects": res}
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def op_devices_list(args):
|
|
829
|
+
"""The device tree with identification and state, and each device's I/O mapping
|
|
830
|
+
as CODESYS exports it (export_io_mappings_as_csv: 'CoDeSys Mapping Export V1.2').
|
|
831
|
+
Read-only: the CSV goes to a temp file in the session folder."""
|
|
832
|
+
c = Ctx()
|
|
833
|
+
tmp = os.environ.get("CODESYS_MCP_DIR") or os.environ.get("TEMP") or "."
|
|
834
|
+
out = []
|
|
835
|
+
|
|
836
|
+
def rec(obj, names, depth):
|
|
837
|
+
for ch in children(obj):
|
|
838
|
+
nm = name_of(ch)
|
|
839
|
+
if _safe(lambda: ch.is_device, False):
|
|
840
|
+
ident = _safe(lambda: ch.get_device_identification())
|
|
841
|
+
info = {
|
|
842
|
+
"path": "/".join(names + [nm]),
|
|
843
|
+
"name": nm,
|
|
844
|
+
"depth": depth,
|
|
845
|
+
"type": _safe(lambda: int(ident.type)),
|
|
846
|
+
"id": to_text(_safe(lambda: ident.id)),
|
|
847
|
+
"version": to_text(_safe(lambda: ident.version)),
|
|
848
|
+
"enabled": _safe(lambda: bool(ch.is_enabled())),
|
|
849
|
+
}
|
|
850
|
+
if args.get("io", True):
|
|
851
|
+
p = os.path.join(tmp, "io-mapping-%d.csv" % len(out))
|
|
852
|
+
try:
|
|
853
|
+
ch.export_io_mappings_as_csv(p)
|
|
854
|
+
with open(p) as f:
|
|
855
|
+
text = f.read()
|
|
856
|
+
if any(l.strip() and not l.startswith("//") for l in text.splitlines()):
|
|
857
|
+
info["io_csv"] = text
|
|
858
|
+
except Exception as e:
|
|
859
|
+
info["io_error"] = to_text(e)
|
|
860
|
+
finally:
|
|
861
|
+
_safe(lambda: os.remove(p))
|
|
862
|
+
out.append(info)
|
|
863
|
+
rec(ch, names + [nm], depth + 1)
|
|
864
|
+
elif kind_of(ch, c.app_guid) in ("folder", "other"):
|
|
865
|
+
rec(ch, names + [nm], depth) # devices sit under "Plc Logic" and folders
|
|
866
|
+
rec(c.proj, [], 0)
|
|
867
|
+
return {"devices": out}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _task_info(c, t):
|
|
871
|
+
wd = _safe(lambda: t.watchdog)
|
|
872
|
+
return {
|
|
873
|
+
"name": name_of(t),
|
|
874
|
+
"path": c.path_of(t),
|
|
875
|
+
"interval": to_text(_safe(lambda: t.interval)),
|
|
876
|
+
"interval_unit": to_text(_safe(lambda: t.interval_unit)),
|
|
877
|
+
"priority": to_text(_safe(lambda: t.priority)),
|
|
878
|
+
"kind": to_text(_safe(lambda: t.kind_of_task)),
|
|
879
|
+
"event": to_text(_safe(lambda: t.event)) or None,
|
|
880
|
+
"core_binding": _safe(lambda: int(t.core_binding)),
|
|
881
|
+
"watchdog": {
|
|
882
|
+
"enabled": bool(_safe(lambda: wd.enabled, False)),
|
|
883
|
+
"time": to_text(_safe(lambda: wd.time)),
|
|
884
|
+
"time_unit": to_text(_safe(lambda: wd.time_unit)),
|
|
885
|
+
"sensitivity": to_text(_safe(lambda: wd.sensitivity)),
|
|
886
|
+
} if wd is not None else None,
|
|
887
|
+
"pous": [to_text(getattr(p, "pou_name", None) or getattr(p, "name", None) or p)
|
|
888
|
+
for p in (_safe(lambda: list(t.pous), []) or [])],
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def _find_kind(c, root, kind):
|
|
893
|
+
out = []
|
|
894
|
+
|
|
895
|
+
def rec(obj):
|
|
896
|
+
for ch in children(obj):
|
|
897
|
+
k = kind_of(ch, c.app_guid)
|
|
898
|
+
if k == kind:
|
|
899
|
+
out.append(ch)
|
|
900
|
+
if k in ("task_config", "folder"):
|
|
901
|
+
rec(ch)
|
|
902
|
+
rec(root)
|
|
903
|
+
return out
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
_TASK_KINDS = {"cyclic": "Cyclic", "freewheeling": "Freewheeling", "event": "Event", "external_event": "ExternalEvent", "status": "Status"}
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def op_task_configure(args):
|
|
910
|
+
"""Create or change a task: kind, interval, priority, event, watchdog, and its program calls."""
|
|
911
|
+
c = Ctx()
|
|
912
|
+
if c.app is None:
|
|
913
|
+
raise OpError("no_application", "The project has no active application.")
|
|
914
|
+
name = args["name"]
|
|
915
|
+
tasks = [t for t in _find_kind(c, c.app, "task") if name_of(t).lower() == name.lower()]
|
|
916
|
+
created = False
|
|
917
|
+
if tasks:
|
|
918
|
+
task = tasks[0]
|
|
919
|
+
elif args.get("create"):
|
|
920
|
+
cfgs = _find_kind(c, c.app, "task_config")
|
|
921
|
+
cfg = cfgs[0] if cfgs else c.app.create_task_configuration()
|
|
922
|
+
task = cfg.create_task(name)
|
|
923
|
+
created = True
|
|
924
|
+
else:
|
|
925
|
+
known = ", ".join(name_of(t) for t in _find_kind(c, c.app, "task")) or "none"
|
|
926
|
+
raise OpError("not_found", "No task %s (tasks: %s). Pass create to make it." % (name, known))
|
|
927
|
+
|
|
928
|
+
if args.get("kind"):
|
|
929
|
+
k = _TASK_KINDS.get(args["kind"].lower())
|
|
930
|
+
if k is None:
|
|
931
|
+
raise OpError("bad_args", "kind must be one of %s" % ", ".join(sorted(_TASK_KINDS)))
|
|
932
|
+
task.kind_of_task = getattr(se.KindOfTask, k)
|
|
933
|
+
for key in ("interval", "interval_unit", "priority", "event"):
|
|
934
|
+
if args.get(key) is not None:
|
|
935
|
+
setattr(task, key, to_text(args[key]))
|
|
936
|
+
if args.get("core_binding") is not None:
|
|
937
|
+
task.core_binding = int(args["core_binding"])
|
|
938
|
+
wd = args.get("watchdog")
|
|
939
|
+
if wd:
|
|
940
|
+
w = task.watchdog
|
|
941
|
+
if wd.get("enabled") is not None:
|
|
942
|
+
w.enabled = bool(wd["enabled"])
|
|
943
|
+
for key in ("time", "time_unit", "sensitivity"):
|
|
944
|
+
if wd.get(key) is not None:
|
|
945
|
+
setattr(w, key, to_text(wd[key]))
|
|
946
|
+
|
|
947
|
+
calls = [to_text(getattr(p, "pou_name", None) or getattr(p, "name", None) or p).lower() for p in list(task.pous)]
|
|
948
|
+
for prog in args.get("remove_calls") or []:
|
|
949
|
+
if prog.lower() not in calls:
|
|
950
|
+
raise OpError("not_found", "Task %s doesn't call %s." % (name_of(task), prog))
|
|
951
|
+
_remove_call(task, calls.index(prog.lower()), prog)
|
|
952
|
+
calls.remove(prog.lower())
|
|
953
|
+
if args.get("add_calls"):
|
|
954
|
+
programs = dict((name_of(p).lower(), name_of(p)) for p in _walk_kind(c, c.app, "pou") if subkind("pou", decl_of(p)) == "program")
|
|
955
|
+
for prog in args["add_calls"]:
|
|
956
|
+
real = programs.get(prog.lower())
|
|
957
|
+
if real is None:
|
|
958
|
+
raise OpError("not_found", "%s is not a PROGRAM in the application; a task can only call programs." % prog)
|
|
959
|
+
if real.lower() in calls:
|
|
960
|
+
continue
|
|
961
|
+
task.pous.add(real, "")
|
|
962
|
+
calls.append(real.lower())
|
|
963
|
+
info = _task_info(c, task)
|
|
964
|
+
info["created"] = created
|
|
965
|
+
info["saved"] = c.save_if(args)
|
|
966
|
+
return info
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _remove_call(task, index, name):
|
|
970
|
+
"""Remove a task's program call, and check that it is gone. CODESYS V3.5 SP21's
|
|
971
|
+
scripting can't do this: pous.remove(name) returns without removing anything,
|
|
972
|
+
and pous.remove(index) fails with "not found in the specified Collection"
|
|
973
|
+
(verified 9 November 2026). Say so instead of pretending."""
|
|
974
|
+
before = [to_text(x).lower() for x in task.pous]
|
|
975
|
+
for attempt in (lambda: task.pous.remove(name), lambda: task.pous.remove(index)):
|
|
976
|
+
try:
|
|
977
|
+
attempt()
|
|
978
|
+
except Exception:
|
|
979
|
+
pass
|
|
980
|
+
if len([x for x in task.pous]) < len(before):
|
|
981
|
+
return
|
|
982
|
+
raise OpError(
|
|
983
|
+
"not_supported",
|
|
984
|
+
"CODESYS's scripting can't remove a program call from a task (Scripting 4.1 and 4.2, on SP21 and SP22: "
|
|
985
|
+
"remove() leaves the call in place). Remove it in CODESYS: Task Configuration > %s > select %s > Remove Call. "
|
|
986
|
+
"Adding calls works." % (name_of(task), name))
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
_TEST_ATTR = re.compile(r"\{\s*attribute\s+'test'\s*\}", re.I)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _walk_all(c, root):
|
|
993
|
+
"""Every object directly in root or in its folders (not inside POUs)."""
|
|
994
|
+
out = []
|
|
995
|
+
|
|
996
|
+
def rec(obj):
|
|
997
|
+
for ch in children(obj):
|
|
998
|
+
if kind_of(ch, c.app_guid) == "folder":
|
|
999
|
+
rec(ch)
|
|
1000
|
+
else:
|
|
1001
|
+
out.append(ch)
|
|
1002
|
+
rec(root)
|
|
1003
|
+
return out
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def _walk_kind(c, root, kind):
|
|
1007
|
+
"""Every object of a kind below root, through folders."""
|
|
1008
|
+
out = []
|
|
1009
|
+
|
|
1010
|
+
def rec(obj):
|
|
1011
|
+
for ch in children(obj):
|
|
1012
|
+
k = kind_of(ch, c.app_guid)
|
|
1013
|
+
if k == kind:
|
|
1014
|
+
out.append(ch)
|
|
1015
|
+
if k == "folder":
|
|
1016
|
+
rec(ch)
|
|
1017
|
+
rec(root)
|
|
1018
|
+
return out
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _members(obj):
|
|
1022
|
+
"""Public member names of a scripting object (names only; nothing is called).
|
|
1023
|
+
CODESYS's script objects are dynamic, so dir() can be empty: then the .NET
|
|
1024
|
+
type's members are listed instead."""
|
|
1025
|
+
if obj is None:
|
|
1026
|
+
return None
|
|
1027
|
+
names = [n for n in (_safe(lambda: dir(obj), []) or []) if not n.startswith("_")]
|
|
1028
|
+
if not names:
|
|
1029
|
+
t = _safe(lambda: obj.GetType())
|
|
1030
|
+
if t is not None:
|
|
1031
|
+
names = [to_text(m.Name) for m in (_safe(lambda: t.GetMembers(), []) or [])]
|
|
1032
|
+
names = ["%s" % n for n in names if n and not n.startswith("_") and not n.startswith("get_") and not n.startswith("set_") and n not in (".ctor", "GetType", "ToString", "Equals", "GetHashCode")]
|
|
1033
|
+
names.append("<.NET %s>" % to_text(_safe(lambda: t.FullName)))
|
|
1034
|
+
return sorted(set(names))
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _versionish(obj):
|
|
1038
|
+
"""Values of non-callable members named like a version or define (to compare CODESYS versions)."""
|
|
1039
|
+
out = {}
|
|
1040
|
+
for n in _members(obj) or []:
|
|
1041
|
+
low = n.lower()
|
|
1042
|
+
if "version" not in low and "define" not in low:
|
|
1043
|
+
continue
|
|
1044
|
+
v = _safe(lambda: getattr(obj, n))
|
|
1045
|
+
if v is None or callable(v):
|
|
1046
|
+
continue
|
|
1047
|
+
out[n] = to_text(v)
|
|
1048
|
+
return out
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _reflect_api(path):
|
|
1052
|
+
"""Every public type and member of the loaded scripting assemblies, through .NET
|
|
1053
|
+
reflection, in the shape of system.dump_scripting_api (which Scripting 4.2 removed)."""
|
|
1054
|
+
try:
|
|
1055
|
+
import System # IronPython only
|
|
1056
|
+
except ImportError:
|
|
1057
|
+
return "not available (no .NET)"
|
|
1058
|
+
import json
|
|
1059
|
+
types = []
|
|
1060
|
+
for asm in System.AppDomain.CurrentDomain.GetAssemblies():
|
|
1061
|
+
aname = to_text(_safe(lambda: asm.GetName().Name, ""))
|
|
1062
|
+
if "script" not in aname.lower():
|
|
1063
|
+
continue
|
|
1064
|
+
exported = _safe(lambda: asm.GetExportedTypes(), []) or []
|
|
1065
|
+
for t in exported:
|
|
1066
|
+
members = []
|
|
1067
|
+
for m in _safe(lambda: t.GetMembers(), []) or []:
|
|
1068
|
+
mt = to_text(m.MemberType)
|
|
1069
|
+
if mt == "Method":
|
|
1070
|
+
params = [{"Type": to_text(_safe(lambda: p.ParameterType.FullName) or _safe(lambda: p.ParameterType.Name))} for p in (_safe(lambda: m.GetParameters(), []) or [])]
|
|
1071
|
+
rt = _safe(lambda: m.ReturnType)
|
|
1072
|
+
elif mt == "Property":
|
|
1073
|
+
params = [{"Type": to_text(_safe(lambda: p.ParameterType.FullName) or _safe(lambda: p.ParameterType.Name))} for p in (_safe(lambda: m.GetIndexParameters(), []) or [])]
|
|
1074
|
+
rt = _safe(lambda: m.PropertyType)
|
|
1075
|
+
else:
|
|
1076
|
+
continue
|
|
1077
|
+
members.append({"Name": to_text(m.Name), "MemberType": mt, "Parameters": params,
|
|
1078
|
+
"Type": to_text(_safe(lambda: rt.FullName) or _safe(lambda: rt.Name)) if rt is not None else None})
|
|
1079
|
+
types.append({"Namespace": to_text(t.Namespace), "Name": to_text(t.Name), "Assembly": aname, "Members": members})
|
|
1080
|
+
doc = {"MetaData": {"Generator": "reflection, %s" % to_text(sys.version).replace("\n", " ")[:120]}, "Types": types}
|
|
1081
|
+
text = json.dumps(doc)
|
|
1082
|
+
with open(path, "w") as f:
|
|
1083
|
+
f.write(text.encode("utf-8") if PY2 and isinstance(text, unicode) else text) # noqa: F821
|
|
1084
|
+
return "%d types" % len(types)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def op_api_probe(args):
|
|
1088
|
+
"""The scripting API this CODESYS offers: member names of the main objects, the
|
|
1089
|
+
enums the watcher relies on, version-like values, and the object type GUIDs of
|
|
1090
|
+
the open project with their kinds. For comparing service packs (docs/sp22-plan.md)."""
|
|
1091
|
+
c = Ctx(require_project=False)
|
|
1092
|
+
objs = {"se": se, "system": _safe(lambda: se.system), "projects": _safe(lambda: se.projects), "project": c.proj, "application": c.app}
|
|
1093
|
+
samples = {}
|
|
1094
|
+
types = {}
|
|
1095
|
+
if c.proj is not None:
|
|
1096
|
+
stack = [c.proj]
|
|
1097
|
+
while stack and len(types) < 200:
|
|
1098
|
+
o = stack.pop()
|
|
1099
|
+
t = (to_text(_safe(lambda: o.type)) or "").strip("{}").lower()
|
|
1100
|
+
k = kind_of(o, c.app_guid)
|
|
1101
|
+
if t and t not in types:
|
|
1102
|
+
types[t] = {"kind": k, "example": name_of(o)}
|
|
1103
|
+
if k in ("pou", "method", "property", "accessor", "action", "dut", "gvl", "interface", "folder") and k not in samples:
|
|
1104
|
+
samples[k] = o
|
|
1105
|
+
stack.extend(_safe(lambda: children(o), []) or [])
|
|
1106
|
+
for k, o in samples.items():
|
|
1107
|
+
objs["obj_" + k] = o
|
|
1108
|
+
pou = samples.get("pou")
|
|
1109
|
+
if pou is not None:
|
|
1110
|
+
objs["textual_declaration"] = _safe(lambda: pou.textual_declaration)
|
|
1111
|
+
objs["textual_implementation"] = _safe(lambda: pou.textual_implementation)
|
|
1112
|
+
enums = {}
|
|
1113
|
+
for n in ("VersionUpdateFlags", "PouType", "DutType", "ScriptPromptHandling", "PromptHandling", "ImplementationLanguages", "ProjectProperties"):
|
|
1114
|
+
e = _safe(lambda: getattr(se, n))
|
|
1115
|
+
enums[n] = _members(e) if e is not None else None
|
|
1116
|
+
reflected = None
|
|
1117
|
+
if args.get("reflect_path"):
|
|
1118
|
+
reflected = _reflect_api(args["reflect_path"])
|
|
1119
|
+
dump = None
|
|
1120
|
+
if args.get("dump_path"):
|
|
1121
|
+
# CODESYS's own description of its scripting API (system.dump_scripting_api).
|
|
1122
|
+
fn = _safe(lambda: se.system.dump_scripting_api)
|
|
1123
|
+
if fn is None:
|
|
1124
|
+
dump = "not available"
|
|
1125
|
+
else:
|
|
1126
|
+
try:
|
|
1127
|
+
r = fn(args["dump_path"])
|
|
1128
|
+
dump = "written" if os.path.exists(args["dump_path"]) else ("returned: %s" % to_text(r)[:200])
|
|
1129
|
+
except Exception as e:
|
|
1130
|
+
try:
|
|
1131
|
+
r = fn()
|
|
1132
|
+
text = to_text(r) or ""
|
|
1133
|
+
if text:
|
|
1134
|
+
with open(args["dump_path"], "w") as f:
|
|
1135
|
+
f.write(text.encode("utf-8") if PY2 else text)
|
|
1136
|
+
dump = "returned %d chars (no-argument form; with a path: %s)" % (len(text), to_text(e))
|
|
1137
|
+
except Exception as e2:
|
|
1138
|
+
dump = "failed: %s / %s" % (to_text(e), to_text(e2))
|
|
1139
|
+
return {
|
|
1140
|
+
"python": to_text(_safe(lambda: sys.version, "")).replace("\n", " ")[:120],
|
|
1141
|
+
"dump": dump,
|
|
1142
|
+
"reflected": reflected,
|
|
1143
|
+
"members": dict((k, _members(v)) for k, v in objs.items()),
|
|
1144
|
+
"versions": dict((k, _versionish(v)) for k, v in objs.items() if v is not None),
|
|
1145
|
+
"enums": enums,
|
|
1146
|
+
"types": types,
|
|
1147
|
+
"unknown_types": sorted(t for t, v in types.items() if v["kind"] == "other"),
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
OPS = {
|
|
1152
|
+
"api.probe": op_api_probe,
|
|
1153
|
+
"ping": op_ping,
|
|
1154
|
+
"project.open": op_project_open,
|
|
1155
|
+
"project.open_new": op_project_open_new,
|
|
1156
|
+
"project.create_empty": op_project_create_empty,
|
|
1157
|
+
"project.info": op_project_info,
|
|
1158
|
+
"project.save": op_project_save,
|
|
1159
|
+
"project.close": op_project_close,
|
|
1160
|
+
"tree": op_tree,
|
|
1161
|
+
"objects.read": op_objects_read,
|
|
1162
|
+
"object.write": op_object_write,
|
|
1163
|
+
"object.create": op_object_create,
|
|
1164
|
+
"object.delete": op_object_delete,
|
|
1165
|
+
"object.rename": op_object_rename,
|
|
1166
|
+
"export": op_export,
|
|
1167
|
+
"build": op_build,
|
|
1168
|
+
"libraries.list": op_libraries_list,
|
|
1169
|
+
"libraries.add": op_libraries_add,
|
|
1170
|
+
"libraries.remove": op_libraries_remove,
|
|
1171
|
+
"tasks.list": op_tasks_list,
|
|
1172
|
+
"task.configure": op_task_configure,
|
|
1173
|
+
"devices.list": op_devices_list,
|
|
1174
|
+
"objects.xml": op_objects_xml,
|
|
1175
|
+
"object.import_xml": op_object_import_xml,
|
|
1176
|
+
"library.api": op_library_api,
|
|
1177
|
+
"library.find": op_library_find,
|
|
1178
|
+
}
|