@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.
@@ -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)
@@ -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
- for i, el in enumerate(args.get("elements") or []):
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,