@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.
- package/CHANGELOG.md +100 -0
- package/README.md +190 -10
- package/README.zh-CN.md +254 -0
- package/dist/actions.d.ts +9 -4
- package/dist/actions.js +1 -1
- package/dist/actions.js.map +1 -1
- package/dist/bridge/bridge.d.ts +12 -1
- package/dist/bridge/bridge.js +77 -11
- package/dist/bridge/bridge.js.map +1 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/codesys/debug.d.ts +5 -0
- package/dist/codesys/debug.js +89 -0
- package/dist/codesys/debug.js.map +1 -0
- package/dist/codesys/device-tools.d.ts +3 -0
- package/dist/codesys/device-tools.js +38 -0
- package/dist/codesys/device-tools.js.map +1 -0
- package/dist/codesys/graphical.d.ts +76 -0
- package/dist/codesys/graphical.js +226 -0
- package/dist/codesys/graphical.js.map +1 -0
- package/dist/codesys/lang.d.ts +39 -0
- package/dist/codesys/lang.js +177 -0
- package/dist/codesys/lang.js.map +1 -0
- package/dist/codesys/simtest.d.ts +48 -0
- package/dist/codesys/simtest.js +103 -0
- package/dist/codesys/simtest.js.map +1 -0
- package/dist/codesys/st.d.ts +1 -1
- package/dist/codesys/st.js.map +1 -1
- package/dist/codesys/trace.d.ts +19 -0
- package/dist/codesys/trace.js +139 -0
- package/dist/codesys/trace.js.map +1 -0
- package/dist/hmi/native.d.ts +89 -0
- package/dist/hmi/native.js +447 -0
- package/dist/hmi/native.js.map +1 -0
- package/dist/hmi/render.d.ts +90 -0
- package/dist/hmi/render.js +307 -0
- package/dist/hmi/render.js.map +1 -0
- package/dist/hmi/route.d.ts +32 -0
- package/dist/hmi/route.js +184 -0
- package/dist/hmi/route.js.map +1 -0
- package/dist/hmi/service.d.ts +45 -0
- package/dist/hmi/service.js +182 -0
- package/dist/hmi/service.js.map +1 -0
- package/dist/hmi/spec.d.ts +821 -0
- package/dist/hmi/spec.js +191 -0
- package/dist/hmi/spec.js.map +1 -0
- package/dist/hmi/templates.d.ts +11 -0
- package/dist/hmi/templates.js +92 -0
- package/dist/hmi/templates.js.map +1 -0
- package/dist/hmi/tools.d.ts +4 -0
- package/dist/hmi/tools.js +222 -0
- package/dist/hmi/tools.js.map +1 -0
- package/dist/hmi/value.d.ts +3 -0
- package/dist/hmi/value.js +26 -0
- package/dist/hmi/value.js.map +1 -0
- package/dist/panel/hmi.js +138 -0
- package/dist/panel.d.ts +3 -0
- package/dist/panel.js +17 -0
- package/dist/panel.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +5 -1
- package/dist/tools.js +19 -3
- package/dist/tools.js.map +1 -1
- package/dist/viewer.d.ts +1 -1
- package/dist/viewer.js +13 -2
- package/dist/viewer.js.map +1 -1
- package/package.json +1 -1
- package/watcher/mcp_alarms.py +134 -0
- package/watcher/mcp_debug.py +161 -0
- package/watcher/mcp_devices.py +83 -0
- package/watcher/mcp_lang.py +179 -0
- package/watcher/mcp_ops.py +3 -1
- package/watcher/mcp_trace.py +90 -0
- package/watcher/mcp_ui.py +54 -0
- package/watcher/mcp_visu.py +422 -0
- 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
|
+
})
|
package/watcher/watcher.py
CHANGED
|
@@ -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:
|