@codesys/mcp-toolkit 2.1.2 → 2.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +190 -10
  3. package/README.zh-CN.md +254 -0
  4. package/dist/actions.d.ts +9 -4
  5. package/dist/actions.js +1 -1
  6. package/dist/actions.js.map +1 -1
  7. package/dist/bridge/bridge.d.ts +12 -1
  8. package/dist/bridge/bridge.js +77 -11
  9. package/dist/bridge/bridge.js.map +1 -1
  10. package/dist/cli.js +7 -1
  11. package/dist/cli.js.map +1 -1
  12. package/dist/codesys/debug.d.ts +5 -0
  13. package/dist/codesys/debug.js +89 -0
  14. package/dist/codesys/debug.js.map +1 -0
  15. package/dist/codesys/device-tools.d.ts +3 -0
  16. package/dist/codesys/device-tools.js +38 -0
  17. package/dist/codesys/device-tools.js.map +1 -0
  18. package/dist/codesys/graphical.d.ts +76 -0
  19. package/dist/codesys/graphical.js +226 -0
  20. package/dist/codesys/graphical.js.map +1 -0
  21. package/dist/codesys/lang.d.ts +39 -0
  22. package/dist/codesys/lang.js +177 -0
  23. package/dist/codesys/lang.js.map +1 -0
  24. package/dist/codesys/simtest.d.ts +48 -0
  25. package/dist/codesys/simtest.js +103 -0
  26. package/dist/codesys/simtest.js.map +1 -0
  27. package/dist/codesys/st.d.ts +1 -1
  28. package/dist/codesys/st.js.map +1 -1
  29. package/dist/codesys/trace.d.ts +19 -0
  30. package/dist/codesys/trace.js +139 -0
  31. package/dist/codesys/trace.js.map +1 -0
  32. package/dist/hmi/native.d.ts +89 -0
  33. package/dist/hmi/native.js +447 -0
  34. package/dist/hmi/native.js.map +1 -0
  35. package/dist/hmi/render.d.ts +90 -0
  36. package/dist/hmi/render.js +307 -0
  37. package/dist/hmi/render.js.map +1 -0
  38. package/dist/hmi/route.d.ts +32 -0
  39. package/dist/hmi/route.js +184 -0
  40. package/dist/hmi/route.js.map +1 -0
  41. package/dist/hmi/service.d.ts +45 -0
  42. package/dist/hmi/service.js +182 -0
  43. package/dist/hmi/service.js.map +1 -0
  44. package/dist/hmi/spec.d.ts +821 -0
  45. package/dist/hmi/spec.js +191 -0
  46. package/dist/hmi/spec.js.map +1 -0
  47. package/dist/hmi/templates.d.ts +11 -0
  48. package/dist/hmi/templates.js +92 -0
  49. package/dist/hmi/templates.js.map +1 -0
  50. package/dist/hmi/tools.d.ts +4 -0
  51. package/dist/hmi/tools.js +222 -0
  52. package/dist/hmi/tools.js.map +1 -0
  53. package/dist/hmi/value.d.ts +3 -0
  54. package/dist/hmi/value.js +26 -0
  55. package/dist/hmi/value.js.map +1 -0
  56. package/dist/panel/hmi.js +138 -0
  57. package/dist/panel.d.ts +3 -0
  58. package/dist/panel.js +17 -0
  59. package/dist/panel.js.map +1 -1
  60. package/dist/server.d.ts +1 -1
  61. package/dist/server.js +7 -2
  62. package/dist/server.js.map +1 -1
  63. package/dist/tools.d.ts +5 -1
  64. package/dist/tools.js +19 -3
  65. package/dist/tools.js.map +1 -1
  66. package/dist/viewer.d.ts +1 -1
  67. package/dist/viewer.js +13 -2
  68. package/dist/viewer.js.map +1 -1
  69. package/package.json +1 -1
  70. package/watcher/mcp_alarms.py +134 -0
  71. package/watcher/mcp_debug.py +161 -0
  72. package/watcher/mcp_devices.py +83 -0
  73. package/watcher/mcp_lang.py +179 -0
  74. package/watcher/mcp_ops.py +3 -1
  75. package/watcher/mcp_trace.py +90 -0
  76. package/watcher/mcp_ui.py +54 -0
  77. package/watcher/mcp_visu.py +422 -0
  78. package/watcher/watcher.py +52 -10
@@ -0,0 +1,422 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Native CODESYS visualizations, built from a plan the server computes (src/hmi/native.ts).
3
+ #
4
+ # Two routes, both CODESYS's own:
5
+ # - the visualization script driver (create_visualobject, add_element, set_property with
6
+ # property-view paths) for image switchers, bindings, colour toggles, movement, inputs;
7
+ # - CODESYS's element creators (the objects its toolbox uses) for what the property paths
8
+ # can't set: a static font, a gradient, a hidden frame or fill. Added through CODESYS's
9
+ # object manager, like the toolbox does.
10
+ # Rounded corners and the canvas background are members of CODESYS's element objects that
11
+ # were checked alone. Nothing here writes other raw members (some raise assertion dialogs).
12
+ # A property that fails is reported, not fatal. render paints a visualization into a PNG.
13
+ #
14
+ # Keep this file ASCII-only (IronPython 2.7 inside CODESYS).
15
+ import os
16
+
17
+ import mcp_ops
18
+ from mcp_ops import Ctx, OpError, name_of
19
+ from mcp_compat import to_text
20
+
21
+ import scriptengine as se
22
+
23
+ _CACHE = {}
24
+
25
+
26
+ def _find_type(name):
27
+ if ("t", name) in _CACHE:
28
+ return _CACHE[("t", name)]
29
+ from System import AppDomain
30
+ found = None
31
+ for asm in AppDomain.CurrentDomain.GetAssemblies():
32
+ try:
33
+ found = asm.GetType(name)
34
+ except Exception:
35
+ found = None
36
+ if found is not None:
37
+ break
38
+ _CACHE[("t", name)] = found
39
+ return found
40
+
41
+
42
+ def _static(type_name, prop):
43
+ from System.Reflection import BindingFlags
44
+ t = _find_type(type_name)
45
+ if t is None:
46
+ return None
47
+ p = t.GetProperty(prop, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
48
+ return p.GetValue(None, None) if p is not None else None
49
+
50
+
51
+ def _object_mgr():
52
+ for t in ("_3S.CoDeSys.VisualEditor.APEnvironment", "_3S.CoDeSys.SecurityManager.APEnvironment"):
53
+ m = _static(t, "ObjectMgr")
54
+ if m is not None:
55
+ return m
56
+ raise OpError("unavailable", "CODESYS's object manager isn't reachable.")
57
+
58
+
59
+ def _drawing():
60
+ import clr
61
+ clr.AddReference("System.Drawing")
62
+ import System.Drawing as SD
63
+ return SD
64
+
65
+
66
+ def _argb(v):
67
+ import System
68
+ v = int(v)
69
+ return _drawing().Color.FromArgb(System.Int32(v - 0x100000000 if v >= 0x80000000 else v))
70
+
71
+
72
+ def _value(v):
73
+ if isinstance(v, dict) and "argb" in v:
74
+ return _argb(v["argb"])
75
+ return v
76
+
77
+
78
+ def _enum(type_name, wanted):
79
+ import System
80
+ t = _find_type(type_name)
81
+ if t is None:
82
+ raise OpError("unavailable", "%s not found" % type_name)
83
+ names = list(System.Enum.GetNames(t))
84
+ for n in names:
85
+ if n == wanted:
86
+ return System.Enum.Parse(t, n)
87
+ for n in names:
88
+ if wanted in n:
89
+ return System.Enum.Parse(t, n)
90
+ raise OpError("bad_args", "%s has no %s (%s)" % (type_name, wanted, ", ".join(names)))
91
+
92
+
93
+ def _pset(o, name, value):
94
+ """Set a property, also one implemented explicitly on an interface."""
95
+ t = o.GetType()
96
+ for it in [t] + list(t.GetInterfaces()):
97
+ p = it.GetProperty(name)
98
+ if p is not None and p.CanWrite:
99
+ p.SetValue(o, value, None)
100
+ return
101
+ raise AttributeError(name)
102
+
103
+
104
+ class _Modify(object):
105
+ def __init__(self, proj, obj):
106
+ self.h, self.g = proj.handle, obj.guid
107
+
108
+ def __enter__(self):
109
+ self.mgr = _object_mgr()
110
+ self.mo = self.mgr.GetObjectToModify(self.h, self.g)
111
+ return self.mo.Object
112
+
113
+ def __exit__(self, et, ev, tb):
114
+ self.mgr.SetObject(self.mo, et is None, None)
115
+ return False
116
+
117
+
118
+ def _creator(kind):
119
+ import System
120
+ from System.Reflection import BindingFlags
121
+ t = _find_type("_3S.CoDeSys.ScriptDriverVisualization.APEnvironment")
122
+ if t is None:
123
+ raise OpError("unavailable", "The visualization script driver isn't loaded.")
124
+ f = t.GetMethod("CreateVisualElemCreatorFactory", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).Invoke(None, None)
125
+ return f.CreateInstance(kind)
126
+
127
+
128
+ def _gradient(c1, c2, angle):
129
+ import System
130
+ GV = _find_type("_3S.CoDeSys.PropertyView.GradientView")
131
+ k = [k for k in GV.GetConstructors() if len(k.GetParameters()) == 9][0]
132
+ ps = k.GetParameters()
133
+ con_t = ps[0].ParameterType
134
+
135
+ def con(v):
136
+ return System.Activator.CreateInstance(con_t, System.Array[object]([_argb(v)]))
137
+ look = System.Enum.GetValues(ps[4].ParameterType).GetValue(0)
138
+ SD = _drawing()
139
+ return k.Invoke(System.Array[object]([con(c1), con(c2), int(angle), SD.Point(0, 0), look, 1, 0, con(c2), None]))
140
+
141
+
142
+ def _font(spec):
143
+ SD = _drawing()
144
+ name, size = spec[0], float(spec[1])
145
+ style = SD.FontStyle.Bold if len(spec) > 2 and spec[2] == "bold" else SD.FontStyle.Regular
146
+ return SD.Font(name, size, style)
147
+
148
+
149
+ _SIMPLE = {"rect": "RECTANGLE", "round": "ROUNDRECT", "circle": "CIRCLE"}
150
+
151
+
152
+ def _simple_creator(el):
153
+ import System
154
+ SD = _drawing()
155
+ cr = _creator("VisuFbElemSimple")
156
+ _pset(cr, "Type", _enum("_3S.CoDeSys.VisualElem.VisuEnumSimpleType", _SIMPLE[el["kind"]]))
157
+ _pset(cr, "Position", SD.Rectangle(int(el["x"]), int(el["y"]), int(el["w"]), int(el["h"])))
158
+ if el.get("text") is not None:
159
+ _pset(cr, "Text", el["text"])
160
+ _pset(cr, "HorizontalAlignment", _enum("_3S.CoDeSys.VisualElem.VisuEnumHorizontalAlignment", el.get("align") or "HCENTER"))
161
+ if el.get("font"):
162
+ m = cr.GetType().GetInterface("ITextElementCreator").GetMethod("SetFont")
163
+ m.Invoke(cr, System.Array[object]([_font(el["font"]), _argb(el.get("color", 0xFF0F172A))]))
164
+ if el.get("fill") is None:
165
+ try:
166
+ _pset(cr, "FillAttributes", _enum("_3S.CoDeSys.VisualElem.VisuEnumBrushStyle", "NULL"))
167
+ except OpError:
168
+ _pset(cr, "FillAttributes", _enum("_3S.CoDeSys.VisualElem.VisuEnumBrushStyle", "HOLLOW"))
169
+ else:
170
+ _pset(cr, "Color", _argb(el["fill"]))
171
+ if el.get("frame") is None:
172
+ _pset(cr, "FrameAttributes", _enum("_3S.CoDeSys.VisualElem.VisuEnumPenStyle", "HOLLOW"))
173
+ else:
174
+ _pset(cr, "FrameColor", _argb(el["frame"]))
175
+ _pset(cr, "LineWidth", int(el.get("lineWidth") or 1))
176
+ if el.get("gradient"):
177
+ g = el["gradient"]
178
+ _pset(cr, "Gradient", _gradient(g[0], g[1], g[2] if len(g) > 2 else 90))
179
+ _pset(cr, "UseGradient", True)
180
+ if el.get("invisible"):
181
+ _pset(cr, "InvisibleExpression", el["invisible"])
182
+ return cr
183
+
184
+
185
+ def _add_created(proj, visu, cr, name):
186
+ elem = cr.CreateVisualElement(name)
187
+ with _Modify(proj, visu) as vo:
188
+ elem.Id = vo.UniqueId
189
+ elem.OwningObjectGuid = vo.MetaObject.ObjectGuid
190
+ vo.VisualElemList.Add(elem)
191
+ return elem.Id
192
+
193
+
194
+ def _script_element(visu, eid):
195
+ for e in visu.visual_element_list:
196
+ if e.id == eid:
197
+ return e
198
+ return None
199
+
200
+
201
+ def _find_child(c, name):
202
+ for ch in c.app.get_children(False):
203
+ if name_of(ch).lower() == name.lower():
204
+ return ch
205
+ return None
206
+
207
+
208
+ def op_visu_build(args):
209
+ c = Ctx()
210
+ if not hasattr(c.app, "create_visualobject"):
211
+ raise OpError("unavailable", "This CODESYS has no visualization scripting (CODESYS Visualization 4.7 or later is needed).")
212
+ name = args["name"]
213
+ for n in (name, args.get("image_pool")):
214
+ if not n:
215
+ continue
216
+ old = _find_child(c, n)
217
+ if old is not None:
218
+ if not args.get("replace"):
219
+ raise OpError("exists", "%s already exists. Pass replace to rebuild it." % n)
220
+ old.remove()
221
+ warnings = []
222
+ if args.get("images"):
223
+ pool = c.app.create_imagepool(args["image_pool"])
224
+ for img in args["images"]:
225
+ try:
226
+ pool.images.add(img["id"], img["path"], se.ImageLinkType.Embedded, se.AutoUpdateMode.Never)
227
+ except Exception as ex:
228
+ warnings.append("image %s: %s" % (img["id"], to_text(ex)))
229
+ visu = c.app.create_visualobject(name)
230
+ if args.get("size") or args.get("dialog"):
231
+ try:
232
+ _visual_property(c, visu, args.get("size"), bool(args.get("dialog")))
233
+ except Exception as ex:
234
+ warnings.append("size/dialog: %s" % to_text(ex))
235
+ T = se.VisualElementType
236
+ corners = []
237
+ for i, el in enumerate(args.get("elements") or []):
238
+ label = el.get("name") or "E%d" % i
239
+ try:
240
+ if el["kind"] in _SIMPLE:
241
+ eid = _add_created(c.proj, visu, _simple_creator(el), label)
242
+ e = _script_element(visu, eid)
243
+ else:
244
+ e = visu.visual_element_list.add_element({"switcher": T.ImageSwitcher, "image": T.Image}[el["kind"]])
245
+ for p, v in (("Position.X", el["x"]), ("Position.Y", el["y"]), ("Position.Width", el["w"]), ("Position.Height", el["h"])):
246
+ e.set_property(p, int(v))
247
+ eid = e.id
248
+ except Exception as ex:
249
+ warnings.append("%s: not added: %s" % (label, to_text(ex)))
250
+ continue
251
+ for path, v in (el.get("props") or {}).items():
252
+ try:
253
+ e.set_property(path, _value(v))
254
+ except Exception as ex:
255
+ if len(warnings) < 40:
256
+ warnings.append("%s %s: %s" % (label, path, to_text(ex)))
257
+ if el["kind"] not in _SIMPLE and (el.get("click") or el.get("goto") or el.get("dialog") or el.get("close_dialog") or el.get("login")):
258
+ # Image elements have no input handling: an input action there makes CODESYS's
259
+ # code generation raise an assertion dialog. Click areas are simple shapes.
260
+ warnings.append("%s: input actions only go on shapes, not on %s" % (label, el["kind"]))
261
+ el = dict(el)
262
+ for k in ("click", "goto", "dialog", "close_dialog", "login"):
263
+ el.pop(k, None)
264
+ if el.get("click"):
265
+ try:
266
+ e.add_input_action(visu.input_action_factory.create_execute_st_code(el["click"]), se.InputActionEventType.OnMouseClick)
267
+ except Exception as ex:
268
+ warnings.append("%s click: %s" % (label, to_text(ex)))
269
+ if el.get("goto"):
270
+ try:
271
+ act = visu.input_action_factory.create_change_shown_visualization(se.ChangeShownVisualizationType.Assignment, el["goto"])
272
+ e.add_input_action(act, se.InputActionEventType.OnMouseClick)
273
+ except Exception as ex:
274
+ warnings.append("%s goto: %s" % (label, to_text(ex)))
275
+ if el.get("dialog"):
276
+ try:
277
+ from System.Collections.Generic import Dictionary, List
278
+ d = el["dialog"]
279
+ act = visu.input_action_factory.create_open_dialog(d["name"], Dictionary[object, object](), List[object](), True, False, str(int(d.get("x", 200))), str(int(d.get("y", 120))))
280
+ e.add_input_action(act, se.InputActionEventType.OnMouseClick)
281
+ except Exception as ex:
282
+ warnings.append("%s dialog: %s" % (label, to_text(ex)))
283
+ if el.get("login"):
284
+ try:
285
+ lt = _find_type("_3S.CoDeSys.VisualElem.VisuUserMgmtInputActionFactory")
286
+ import System
287
+ act = System.Activator.CreateInstance(lt).CreateInputAction()
288
+ e.add_input_action(act, se.InputActionEventType.OnMouseClick)
289
+ except Exception as ex:
290
+ warnings.append("%s login: %s" % (label, to_text(ex)))
291
+ if el.get("close_dialog"):
292
+ try:
293
+ act = visu.input_action_factory.create_close_dialog(el["close_dialog"], "OK")
294
+ e.add_input_action(act, se.InputActionEventType.OnMouseClick)
295
+ except Exception as ex:
296
+ warnings.append("%s close: %s" % (label, to_text(ex)))
297
+ if el.get("radius"):
298
+ corners.append((eid, int(el["radius"])))
299
+ try:
300
+ with _Modify(c.proj, visu) as vo:
301
+ if args.get("background") is not None:
302
+ import System
303
+ bg = vo.VisualElemList.Background
304
+ bg.UseBackgroundColor = True
305
+ v = int(args["background"])
306
+ bg.BackgroundColor = System.Int32(v - 0x100000000 if v >= 0x80000000 else v)
307
+ for eid, r in corners:
308
+ el = vo.FindElementById(eid)
309
+ if el is not None:
310
+ el.GetVisualElemMember("m_Radius.m_iRadius", True, r).Value = r
311
+ except Exception as ex:
312
+ warnings.append("background/corners: %s" % to_text(ex))
313
+ return {"name": name_of(visu), "path": name_of(visu), "kind": "visualization", "elements": len(visu.visual_element_list), "warnings": warnings, "saved": c.save_if(args)}
314
+
315
+
316
+ def _visual_property(c, visu, size, dialog):
317
+ """The visualization's own settings: canvas size, and whether it is a dialog."""
318
+ import System
319
+ SD = _drawing()
320
+ mgr = _object_mgr()
321
+ mo = mgr.GetObjectToModify(c.proj.handle, visu.guid)
322
+ ok = False
323
+ try:
324
+ props = [p for p in mo.Properties if p.GetType().Name == "VisualProperty"]
325
+ if props:
326
+ vp = props[0]
327
+ if size:
328
+ vp.Size = SD.Size(int(size[0]), int(size[1]))
329
+ if dialog:
330
+ vp.UsageType = System.Enum.Parse(_find_type("_3S.CoDeSys.VisualObject.VisualUsageType"), "Dialog")
331
+ ok = True
332
+ finally:
333
+ mgr.SetObject(mo, ok, None)
334
+
335
+
336
+ def op_visu_manager(args):
337
+ """Start page, client size and style of the Visualization Manager, TargetVisu and WebVisu."""
338
+ import System
339
+ c = Ctx()
340
+ SD = _drawing()
341
+ done, warnings = [], []
342
+ style = None
343
+ if args.get("style"):
344
+ repo = _static("_3S.CoDeSys.VisualEditor.APEnvironment", "VisualizationStylesRepository")
345
+ for st in (repo.GetAllStyles() if repo is not None else []):
346
+ nm = to_text(_get(st, "Name") or "")
347
+ if nm.lower() == args["style"].lower():
348
+ style = to_text(_get(st, "DisplayName"))
349
+ if style is None:
350
+ warnings.append("style %s is not installed" % args["style"])
351
+ for name in ("Visualization Manager", "TargetVisu", "WebVisu"):
352
+ obj = _find_child_deep(c, name)
353
+ if obj is None:
354
+ continue
355
+ try:
356
+ with _Modify(c.proj, obj) as vo:
357
+ vs = vo.ViewSettings
358
+ if args.get("start"):
359
+ vs.StartVisu = args["start"]
360
+ if name == "Visualization Manager":
361
+ if style:
362
+ vs.VisualizationStyle = style
363
+ elif args.get("width") and args.get("height"):
364
+ try:
365
+ vs.ClientSizeMode = System.Enum.Parse(vs.ClientSizeMode.GetType(), "Specified")
366
+ except Exception:
367
+ pass
368
+ vs.ClientSize = SD.Size(int(args["width"]), int(args["height"]))
369
+ try:
370
+ vs.BestFit = True
371
+ except Exception:
372
+ pass
373
+ done.append(name)
374
+ except Exception as ex:
375
+ warnings.append("%s: %s" % (name, to_text(ex)))
376
+ return {"updated": done, "warnings": warnings, "saved": c.save_if(args)}
377
+
378
+
379
+ def _get(o, name):
380
+ try:
381
+ return getattr(o, name)
382
+ except Exception:
383
+ return None
384
+
385
+
386
+ def _find_child_deep(c, name):
387
+ for ch in c.app.get_children(True):
388
+ if name_of(ch).lower() == name.lower():
389
+ return ch
390
+ return None
391
+
392
+
393
+ def op_visu_render(args):
394
+ """Paints a visualization into a PNG (no editor or window needed)."""
395
+ c = Ctx()
396
+ visu = _find_child(c, args["name"])
397
+ if visu is None:
398
+ raise OpError("not_found", "No visualization named %s." % args["name"])
399
+ mgr = _object_mgr()
400
+ SD = _drawing()
401
+ w, h = int(args.get("width") or 1280), int(args.get("height") or 720)
402
+ bmp = SD.Bitmap(w, h)
403
+ g = SD.Graphics.FromImage(bmp)
404
+ try:
405
+ g.Clear(SD.Color.White)
406
+ mgr.GetObjectToRead(c.proj.handle, visu.guid).Object.Paint(g, SD.Rectangle(0, 0, w, h))
407
+ finally:
408
+ g.Dispose()
409
+ path = args["path"]
410
+ folder = os.path.dirname(path)
411
+ if folder and not os.path.isdir(folder):
412
+ os.makedirs(folder)
413
+ bmp.Save(path, SD.Imaging.ImageFormat.Png)
414
+ bmp.Dispose()
415
+ return {"path": path}
416
+
417
+
418
+ mcp_ops.OPS.update({
419
+ "visu.manager": op_visu_manager,
420
+ "visu.build": op_visu_build,
421
+ "visu.render": op_visu_render,
422
+ })
@@ -2,6 +2,11 @@
2
2
  # codesys-mcp-toolkit v2 watcher.
3
3
  #
4
4
  # Started by the MCP server as: CODESYS.exe --profile="..." --noUI --runscript="<dir>/watcher.py"
5
+ # (with --show-window: no --noUI and CODESYS_MCP_UI=1. The same blocking loop runs: CODESYS
6
+ # applies script_prompt_handling only while a script runs, so a timer that runs requests after
7
+ # the script returned would show every prompt as a dialog. system.delay() serves the window's
8
+ # message loop, so the window keeps painting while the loop waits. Objects that requests
9
+ # write or create open in an editor tab there; see mcp_ui.py.)
5
10
  # Environment (inherited from the server):
6
11
  # CODESYS_MCP_DIR session folder (requests/, results/, heartbeat.json, ready.json, watcher.log)
7
12
  # CODESYS_MCP_SCRIPTS folder holding this file and its modules
@@ -46,6 +51,24 @@ STATE = {"count": 0, "last_beat": 0.0, "busy_since": None, "op": None}
46
51
  # A dialog must stay up this long before the server is told, so brief windows don't count.
47
52
  DIALOG_GRACE_S = 10.0
48
53
 
54
+ # With the window shown (--show-window), these ops open the object they wrote in an editor.
55
+ UI = {"on": False}
56
+ FOLLOW_OPS = ("object.write", "object.create", "visu.build")
57
+
58
+
59
+ def follow_in_editor(ops_mod, result):
60
+ """Opens the object a request wrote in its editor, so the window shows the code."""
61
+ path = (result or {}).get("path")
62
+ if not path or (result or {}).get("kind") == "folder":
63
+ return
64
+ try:
65
+ import scriptengine as se
66
+ import mcp_ui
67
+ obj = ops_mod.Ctx().resolve(path)
68
+ log("editor %s: %s" % (mcp_ui.open_editor(se, obj), path))
69
+ except BaseException as e:
70
+ log("editor not opened for %s: %s" % (path, to_text(e)))
71
+
49
72
 
50
73
  def log(msg):
51
74
  try:
@@ -97,20 +120,24 @@ def scripting_version():
97
120
  return m.group(1) if m else None
98
121
 
99
122
 
100
- def find_dialog():
123
+ def find_dialog(ui=False):
101
124
  # A session without a UI has no open forms and no main window (checked on SP21 and
102
125
  # SP22), so a modal form or a main window means CODESYS is waiting for an answer.
126
+ # With a window (ui), only a modal form counts.
103
127
  from System.Windows.Forms import Application
104
128
  from System.Diagnostics import Process
105
129
  for f in Application.OpenForms:
106
130
  if f.Modal:
107
131
  return to_text(f.Text) or "(untitled)"
132
+ if ui:
133
+ return None
108
134
  return to_text(Process.GetCurrentProcess().MainWindowTitle) or None
109
135
 
110
136
 
111
- def watch_dialogs():
137
+ def watch_dialogs(ui=False):
112
138
  # Runs beside the request loop. A dialog blocks that loop for good and nobody can
113
139
  # answer it in a session without a window, so tell the server (dialog.json).
140
+ # With a window the user can answer it; the server is still told, so the call ends.
114
141
  import clr
115
142
  clr.AddReference("System.Windows.Forms")
116
143
  title, since, reported, failed = None, 0.0, False, False
@@ -120,7 +147,7 @@ def watch_dialogs():
120
147
  current = None
121
148
  if busy is not None and now() - busy > 2.0:
122
149
  try:
123
- current = find_dialog()
150
+ current = find_dialog(ui)
124
151
  except Exception as e: # e.g. the form list changed while it was read
125
152
  if not failed:
126
153
  log("dialog check failed: %s" % to_text(e))
@@ -137,12 +164,12 @@ def watch_dialogs():
137
164
  reported = True
138
165
 
139
166
 
140
- def start_dialog_watch():
167
+ def start_dialog_watch(ui=False):
141
168
  import threading
142
169
 
143
170
  def run():
144
171
  try:
145
- watch_dialogs()
172
+ watch_dialogs(ui)
146
173
  except BaseException as e:
147
174
  log("dialog watch stopped: %s" % to_text(e))
148
175
  t = threading.Thread(target=run)
@@ -200,6 +227,8 @@ def handle(ops_mod, fname):
200
227
  else:
201
228
  try:
202
229
  res = {"id": rid, "ok": True, "result": fn(args)}
230
+ if UI["on"] and op in FOLLOW_OPS:
231
+ follow_in_editor(ops_mod, res["result"])
203
232
  except ops_mod.OpError as e:
204
233
  res = {"id": rid, "ok": False, "error": {"code": e.code, "message": to_text(e.message)}}
205
234
  except BaseException as e: # IronPython: KeyboardInterrupt is not an Exception
@@ -216,20 +245,27 @@ def handle(ops_mod, fname):
216
245
  return not stop
217
246
 
218
247
 
219
- def main():
248
+ def main(ui=False):
249
+ UI["on"] = ui
220
250
  for d in (REQ_DIR, RES_DIR):
221
251
  if not os.path.isdir(d):
222
252
  os.makedirs(d)
223
- log("watcher starting pid=%s parent=%s python=%s" % (PID, PARENT_PID, sys.version.split()[0]))
253
+ log("watcher starting pid=%s parent=%s python=%s%s" % (PID, PARENT_PID, sys.version.split()[0], " (window shown)" if ui else ""))
224
254
  import scriptengine as se
225
255
  import mcp_ops
226
256
  import mcp_sim # noqa: F401 (adds its operations to mcp_ops.OPS)
257
+ import mcp_visu # noqa: F401 (adds the visualization operations)
258
+ import mcp_lang # noqa: F401 (adds signatures and cross-references)
259
+ import mcp_trace # noqa: F401 (adds trace recording; after mcp_visu)
260
+ import mcp_debug # noqa: F401 (adds breakpoints and stepping)
261
+ import mcp_devices # noqa: F401 (adds device search and add)
262
+ import mcp_alarms # noqa: F401 (adds alarm management and visu user groups)
227
263
  prompts = setup_prompts(se)
228
264
  log(prompts)
229
265
  if not hasattr(se, "FAKE"):
230
- start_dialog_watch()
266
+ start_dialog_watch(ui)
231
267
  beat("idle")
232
- write_json_atomic(READY, {"pid": PID, "ts": now(), "python": sys.version.split()[0], "prompts": prompts, "scripting": scripting_version()})
268
+ write_json_atomic(READY, {"pid": PID, "ts": now(), "python": sys.version.split()[0], "prompts": prompts, "scripting": scripting_version(), "window": ui})
233
269
  last_parent_check = 0.0
234
270
  while True:
235
271
  t = now()
@@ -269,6 +305,12 @@ def main_attach():
269
305
  import scriptengine as se
270
306
  import mcp_ops
271
307
  import mcp_sim # noqa: F401 (adds its operations to mcp_ops.OPS)
308
+ import mcp_visu # noqa: F401 (adds the visualization operations)
309
+ import mcp_lang # noqa: F401 (adds signatures and cross-references)
310
+ import mcp_trace # noqa: F401 (adds trace recording; after mcp_visu)
311
+ import mcp_debug # noqa: F401 (adds breakpoints and stepping)
312
+ import mcp_devices # noqa: F401 (adds device search and add)
313
+ import mcp_alarms # noqa: F401 (adds alarm management and visu user groups)
272
314
  prompts = setup_prompts(se)
273
315
  log(prompts)
274
316
  old = getattr(__builtin__, "_mcp_attach_timer", None)
@@ -317,7 +359,7 @@ try:
317
359
  if os.environ.get("CODESYS_MCP_ATTACH") == "1":
318
360
  main_attach()
319
361
  else:
320
- main()
362
+ main(ui=os.environ.get("CODESYS_MCP_UI") == "1")
321
363
  except BaseException:
322
364
  log("watcher crashed: " + traceback.format_exc())
323
365
  try: