@codesys/mcp-toolkit 2.4.0 → 2.6.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 +34 -0
- package/README.md +15 -9
- package/README.zh-CN.md +11 -6
- package/dist/codesys/device-tools.d.ts +21 -0
- package/dist/codesys/device-tools.js +60 -0
- package/dist/codesys/device-tools.js.map +1 -1
- package/dist/codesys/devices.d.ts +6 -0
- package/dist/codesys/devices.js +24 -1
- package/dist/codesys/devices.js.map +1 -1
- package/dist/codesys/graphical-read.d.ts +63 -0
- package/dist/codesys/graphical-read.js +214 -0
- package/dist/codesys/graphical-read.js.map +1 -0
- package/dist/codesys/graphical.js +13 -1
- package/dist/codesys/graphical.js.map +1 -1
- package/dist/hmi/native.d.ts +14 -2
- package/dist/hmi/native.js +97 -5
- package/dist/hmi/native.js.map +1 -1
- package/dist/hmi/tools.js +25 -16
- package/dist/hmi/tools.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +2 -2
- package/dist/tools.js +20 -5
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/watcher/mcp_devices.py +158 -0
- package/watcher/mcp_visu.py +81 -1
package/watcher/mcp_devices.py
CHANGED
|
@@ -79,5 +79,163 @@ def op_devices_add(args):
|
|
|
79
79
|
return {"path": c.path_of(dev), "saved": c.save_if(args)}
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _param_sets(dev):
|
|
83
|
+
"""(prefix, parameter set) for the device itself and each of its connectors."""
|
|
84
|
+
out = []
|
|
85
|
+
ps = _safe(lambda: dev.device_parameters)
|
|
86
|
+
if ps is not None:
|
|
87
|
+
out.append(("", ps))
|
|
88
|
+
conns = _safe(lambda: dev.connectors) or []
|
|
89
|
+
for i, con in enumerate(conns):
|
|
90
|
+
hp = _safe(lambda: con.host_parameters)
|
|
91
|
+
if hp is not None and len(hp):
|
|
92
|
+
out.append(("c%d/" % i, hp))
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _walk_elements(prefix, el, depth, out, limit):
|
|
97
|
+
"""Every data element under el, depth-first, keyed by its identifier path."""
|
|
98
|
+
if len(out) >= limit:
|
|
99
|
+
return
|
|
100
|
+
ident = to_text(_safe(lambda: el.identifier, "") or "")
|
|
101
|
+
key = prefix + ident
|
|
102
|
+
has_sub = bool(_safe(lambda: el.has_sub_elements, False))
|
|
103
|
+
item = {
|
|
104
|
+
"key": key,
|
|
105
|
+
"name": to_text(_safe(lambda: el.visible_name, "") or ""),
|
|
106
|
+
"depth": depth,
|
|
107
|
+
}
|
|
108
|
+
desc = to_text(_safe(lambda: el.description, "") or "")
|
|
109
|
+
if desc and desc != item["name"]:
|
|
110
|
+
item["description"] = desc[:160]
|
|
111
|
+
unit = to_text(_safe(lambda: el.unit, "") or "")
|
|
112
|
+
if unit:
|
|
113
|
+
item["unit"] = unit
|
|
114
|
+
param = _safe(lambda: el.parameter)
|
|
115
|
+
if depth == 0 and param is not None:
|
|
116
|
+
sec = to_text(_safe(lambda: param.section, "") or "")
|
|
117
|
+
if sec:
|
|
118
|
+
item["section"] = sec
|
|
119
|
+
t = to_text(_safe(lambda: param.type_string, "") or "")
|
|
120
|
+
if t:
|
|
121
|
+
item["type"] = t
|
|
122
|
+
acc = to_text(_safe(lambda: param.offline_access_rights, "") or "")
|
|
123
|
+
if acc:
|
|
124
|
+
item["access"] = acc
|
|
125
|
+
ch = to_text(_safe(lambda: el.parameter.channel_type, "") or "")
|
|
126
|
+
if ch and ch.lower() not in ("none", "0"):
|
|
127
|
+
item["channel"] = ch
|
|
128
|
+
if not has_sub:
|
|
129
|
+
v = _safe(lambda: el.value)
|
|
130
|
+
if v is not None:
|
|
131
|
+
item["value"] = to_text(v)
|
|
132
|
+
d = _safe(lambda: el.default_value)
|
|
133
|
+
if d:
|
|
134
|
+
item["default"] = to_text(d)
|
|
135
|
+
if _safe(lambda: el.is_enumeration, False):
|
|
136
|
+
item["choices"] = [to_text(_safe(lambda: e.identifier, "")) + (("=" + to_text(e.visible_name)) if _safe(lambda: e.visible_name) else "") for e in (_safe(lambda: el.allowed_values) or [])][:40]
|
|
137
|
+
if _safe(lambda: el.is_mappable_io, False):
|
|
138
|
+
m = _safe(lambda: el.io_mapping)
|
|
139
|
+
if m is not None:
|
|
140
|
+
item["mapping"] = {
|
|
141
|
+
"variable": to_text(_safe(lambda: m.variable, "") or ""),
|
|
142
|
+
"address": to_text(_safe(lambda: m.manual_iec_address, "") or ""),
|
|
143
|
+
"automatic_address": bool(_safe(lambda: m.automatic_iec_address, True)),
|
|
144
|
+
}
|
|
145
|
+
out.append(item)
|
|
146
|
+
if has_sub:
|
|
147
|
+
for sub in list(el):
|
|
148
|
+
_walk_elements(key + "/", sub, depth + 1, out, limit)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _device(c, path):
|
|
152
|
+
dev = c.resolve(path)
|
|
153
|
+
if not _safe(lambda: dev.is_device, False):
|
|
154
|
+
raise OpError("bad_path", "%s is not a device (get_devices lists them)." % path)
|
|
155
|
+
return dev
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def op_devices_params(args):
|
|
159
|
+
c = Ctx()
|
|
160
|
+
dev = _device(c, args["path"])
|
|
161
|
+
limit = int(args.get("limit") or 600)
|
|
162
|
+
out = []
|
|
163
|
+
for prefix, ps in _param_sets(dev):
|
|
164
|
+
for p in ps:
|
|
165
|
+
_walk_elements(prefix, p, 0, out, limit)
|
|
166
|
+
return {"path": c.path_of(dev), "parameters": out, "truncated": len(out) >= limit}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _element(dev, key):
|
|
170
|
+
"""The data element with this key (as devices.params gives it)."""
|
|
171
|
+
sets = dict(_param_sets(dev))
|
|
172
|
+
prefix = ""
|
|
173
|
+
rest = key
|
|
174
|
+
if key.startswith("c") and "/" in key and key.split("/")[0][1:].isdigit():
|
|
175
|
+
prefix = key.split("/")[0] + "/"
|
|
176
|
+
rest = key[len(prefix):]
|
|
177
|
+
ps = sets.get(prefix)
|
|
178
|
+
if ps is None:
|
|
179
|
+
raise OpError("not_found", "No parameter set '%s' on this device." % (prefix or "device"))
|
|
180
|
+
parts = rest.split("/")
|
|
181
|
+
el = None
|
|
182
|
+
for p in ps:
|
|
183
|
+
if to_text(_safe(lambda: p.identifier, "")) == parts[0]:
|
|
184
|
+
el = p
|
|
185
|
+
break
|
|
186
|
+
if el is None:
|
|
187
|
+
raise OpError("not_found", "No parameter '%s' (device_parameters lists the keys)." % key)
|
|
188
|
+
for part in parts[1:]:
|
|
189
|
+
nxt = None
|
|
190
|
+
for sub in list(el):
|
|
191
|
+
if to_text(_safe(lambda: sub.identifier, "")) == part:
|
|
192
|
+
nxt = sub
|
|
193
|
+
break
|
|
194
|
+
if nxt is None:
|
|
195
|
+
raise OpError("not_found", "No element '%s' in parameter '%s'." % (part, key))
|
|
196
|
+
el = nxt
|
|
197
|
+
return el
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def op_devices_configure(args):
|
|
201
|
+
c = Ctx()
|
|
202
|
+
dev = _device(c, args["path"])
|
|
203
|
+
done = []
|
|
204
|
+
errors = []
|
|
205
|
+
for v in args.get("values") or []:
|
|
206
|
+
try:
|
|
207
|
+
el = _element(dev, v["key"])
|
|
208
|
+
val = to_text(v["value"])
|
|
209
|
+
if _safe(lambda: el.is_enumeration, False):
|
|
210
|
+
match = None
|
|
211
|
+
for e in el.allowed_values or []:
|
|
212
|
+
if val in (to_text(e.identifier), to_text(_safe(lambda: e.visible_name, "")), to_text(_safe(lambda: e.value, ""))):
|
|
213
|
+
match = e
|
|
214
|
+
break
|
|
215
|
+
if match is None:
|
|
216
|
+
raise OpError("bad_value", "'%s' isn't one of %s" % (val, ", ".join(to_text(e.identifier) for e in el.allowed_values)))
|
|
217
|
+
el.enum_value = match
|
|
218
|
+
else:
|
|
219
|
+
el.value = val
|
|
220
|
+
done.append({"key": v["key"], "value": to_text(_safe(lambda: el.value, val))})
|
|
221
|
+
except Exception as e:
|
|
222
|
+
errors.append({"key": v.get("key"), "error": to_text(e)[:200]})
|
|
223
|
+
for m in args.get("mappings") or []:
|
|
224
|
+
try:
|
|
225
|
+
el = _element(dev, m["key"])
|
|
226
|
+
if not _safe(lambda: el.is_mappable_io, False):
|
|
227
|
+
raise OpError("bad_value", "'%s' is not an I/O channel." % m["key"])
|
|
228
|
+
io = el.io_mapping
|
|
229
|
+
io.variable = to_text(m.get("variable") or "")
|
|
230
|
+
if "address" in m:
|
|
231
|
+
io.manual_iec_address = to_text(m["address"]) if m["address"] else None
|
|
232
|
+
done.append({"key": m["key"], "variable": to_text(_safe(lambda: io.variable, "")), "address": to_text(_safe(lambda: io.manual_iec_address, "") or "")})
|
|
233
|
+
except Exception as e:
|
|
234
|
+
errors.append({"key": m.get("key"), "error": to_text(e)[:200]})
|
|
235
|
+
return {"path": c.path_of(dev), "done": done, "errors": errors, "saved": c.save_if(args)}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
mcp_ops.OPS["devices.params"] = op_devices_params
|
|
239
|
+
mcp_ops.OPS["devices.configure"] = mcp_sim._locked(op_devices_configure)
|
|
82
240
|
mcp_ops.OPS["devices.search"] = op_devices_search
|
|
83
241
|
mcp_ops.OPS["devices.add"] = mcp_sim._locked(op_devices_add)
|
package/watcher/mcp_visu.py
CHANGED
|
@@ -198,6 +198,51 @@ def _script_element(visu, eid):
|
|
|
198
198
|
return None
|
|
199
199
|
|
|
200
200
|
|
|
201
|
+
def _editor_context(c, visu, cache):
|
|
202
|
+
"""The open visual editor's element context for this visualization (needs CODESYS's window)."""
|
|
203
|
+
if "ctx" in cache:
|
|
204
|
+
return cache["ctx"]
|
|
205
|
+
import os
|
|
206
|
+
if os.environ.get("CODESYS_MCP_UI") != "1":
|
|
207
|
+
raise OpError("unavailable", "needs CODESYS's window (start with --show-window)")
|
|
208
|
+
from System.Reflection import BindingFlags
|
|
209
|
+
from System import Guid
|
|
210
|
+
allf = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static
|
|
211
|
+
eng = _static("_3S.CoDeSys.VisualEditor.APEnvironment", "Engine")
|
|
212
|
+
frame = getattr(eng, "Frame", None) if eng is not None else None
|
|
213
|
+
if frame is None:
|
|
214
|
+
raise OpError("unavailable", "needs CODESYS's window (start with --show-window)")
|
|
215
|
+
view = frame.OpenEditorView(_object_mgr().GetMetaObjectStub(c.proj.handle, visu.guid), Guid.Empty, None)
|
|
216
|
+
se.system.delay(800)
|
|
217
|
+
ctl = view.Control
|
|
218
|
+
ve = ctl.GetType().GetField("_visualEditor", allf).GetValue(ctl)
|
|
219
|
+
ctx = ve.GetType().GetField("_editorCtx", allf).GetValue(ve)
|
|
220
|
+
vom = _find_type("_3S.CoDeSys.VisualEditor.VisualObjectManager").GetProperty("Singleton", allf).GetValue(None, None)
|
|
221
|
+
cache["ctx"] = (ctx, vom)
|
|
222
|
+
return cache["ctx"]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _complex_element(c, visu, el, label, cache):
|
|
226
|
+
ctx, vom = _editor_context(c, visu, cache)
|
|
227
|
+
mgr = _object_mgr()
|
|
228
|
+
mo = mgr.GetObjectToModify(c.proj.handle, visu.guid)
|
|
229
|
+
ok = False
|
|
230
|
+
try:
|
|
231
|
+
vo = mo.Object
|
|
232
|
+
nid = vo.UniqueId
|
|
233
|
+
elem = vom.CreateElementByTypeName(el["type_name"], None, label, nid, ctx, True, ctx.AvailableHotkeysProvider, ctx.HotkeysConfiguration)
|
|
234
|
+
elem.Id = nid
|
|
235
|
+
elem.OwningObjectGuid = vo.MetaObject.ObjectGuid
|
|
236
|
+
vo.VisualElemList.Add(elem)
|
|
237
|
+
ok = True
|
|
238
|
+
finally:
|
|
239
|
+
mgr.SetObject(mo, ok, None)
|
|
240
|
+
e = _script_element(visu, nid)
|
|
241
|
+
for p, v in (("Position.X", el["x"]), ("Position.Y", el["y"]), ("Position.Width", el["w"]), ("Position.Height", el["h"])):
|
|
242
|
+
e.set_property(p, int(v))
|
|
243
|
+
return e
|
|
244
|
+
|
|
245
|
+
|
|
201
246
|
def _find_child(c, name):
|
|
202
247
|
for ch in c.app.get_children(False):
|
|
203
248
|
if name_of(ch).lower() == name.lower():
|
|
@@ -234,8 +279,28 @@ def op_visu_build(args):
|
|
|
234
279
|
warnings.append("size/dialog: %s" % to_text(ex))
|
|
235
280
|
T = se.VisualElementType
|
|
236
281
|
corners = []
|
|
237
|
-
|
|
282
|
+
queue = list(args.get("elements") or [])
|
|
283
|
+
editor = {}
|
|
284
|
+
i = -1
|
|
285
|
+
while queue:
|
|
286
|
+
el = queue.pop(0)
|
|
287
|
+
i += 1
|
|
238
288
|
label = el.get("name") or "E%d" % i
|
|
289
|
+
if el["kind"] == "complex":
|
|
290
|
+
# Meters, bar displays and alarm tables need the visual editor's context, which only
|
|
291
|
+
# exists with CODESYS's window. Without it, the plan's fallback elements are drawn.
|
|
292
|
+
try:
|
|
293
|
+
e = _complex_element(c, visu, el, label, editor)
|
|
294
|
+
except Exception as ex:
|
|
295
|
+
warnings.append("%s: drawn without %s (%s)" % (label, el.get("type_name"), to_text(ex)[:160]))
|
|
296
|
+
queue = list(el.get("fallback") or []) + queue
|
|
297
|
+
continue
|
|
298
|
+
for pair in el.get("pairs") or []:
|
|
299
|
+
try:
|
|
300
|
+
e.set_property(pair[0], _value(pair[1]))
|
|
301
|
+
except Exception as ex:
|
|
302
|
+
warnings.append("%s %s: %s" % (label, pair[0], to_text(ex)[:120]))
|
|
303
|
+
continue
|
|
239
304
|
try:
|
|
240
305
|
if el["kind"] in _SIMPLE:
|
|
241
306
|
eid = _add_created(c.proj, visu, _simple_creator(el), label)
|
|
@@ -415,7 +480,22 @@ def op_visu_render(args):
|
|
|
415
480
|
return {"path": path}
|
|
416
481
|
|
|
417
482
|
|
|
483
|
+
def op_ui_open(args):
|
|
484
|
+
"""With CODESYS's window: open an object in its editor (brings its tab forward)."""
|
|
485
|
+
import os
|
|
486
|
+
import mcp_ui
|
|
487
|
+
if os.environ.get("CODESYS_MCP_UI") != "1":
|
|
488
|
+
raise OpError("unavailable", "CODESYS runs without its window. Start the toolkit with --show-window to see objects in CODESYS.")
|
|
489
|
+
c = Ctx()
|
|
490
|
+
obj = c.resolve(args["path"])
|
|
491
|
+
state = mcp_ui.open_editor(se, obj)
|
|
492
|
+
if state == "unavailable":
|
|
493
|
+
raise OpError("unavailable", "This CODESYS doesn't let scripts open editors.")
|
|
494
|
+
return {"path": c.path_of(obj), "opened": state}
|
|
495
|
+
|
|
496
|
+
|
|
418
497
|
mcp_ops.OPS.update({
|
|
498
|
+
"ui.open": op_ui_open,
|
|
419
499
|
"visu.manager": op_visu_manager,
|
|
420
500
|
"visu.build": op_visu_build,
|
|
421
501
|
"visu.render": op_visu_render,
|