1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 # This file is part of Beremiz runtime. |
|
5 # |
|
6 # Copyright (C) 2020: Mario de Sousa |
|
7 # |
|
8 # See COPYING.Runtime file for copyrights details. |
|
9 # |
|
10 # This library is free software; you can redistribute it and/or |
|
11 # modify it under the terms of the GNU Lesser General Public |
|
12 # License as published by the Free Software Foundation; either |
|
13 # version 2.1 of the License, or (at your option) any later version. |
|
14 |
|
15 # This library is distributed in the hope that it will be useful, |
|
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
18 # Lesser General Public License for more details. |
|
19 |
|
20 # You should have received a copy of the GNU Lesser General Public |
|
21 # License along with this library; if not, write to the Free Software |
|
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
|
23 |
|
24 |
|
25 import json |
|
26 import os |
|
27 import ctypes |
|
28 |
|
29 from formless import annotate, webform |
|
30 |
|
31 |
|
32 |
|
33 # reference to the PLCObject in runtime/PLCObject.py |
|
34 # PLCObject is a singleton, created in runtime/__init__.py |
|
35 _plcobj = None |
|
36 |
|
37 # reference to the Nevow web server (a.k.a as NS in Beremiz_service.py) |
|
38 # (Note that NS will reference the NevowServer.py _module_, and not an object/class) |
|
39 _NS = None |
|
40 |
|
41 |
|
42 # WorkingDir: the directory on which Beremiz_service.py is running, and where |
|
43 # all the files downloaded to the PLC get stored |
|
44 _WorkingDir = None |
|
45 |
|
46 |
|
47 # Will contain references to the C functions |
|
48 # (implemented in beremiz/bacnet/runtime/server.c) |
|
49 # used to get/set the BACnet specific configuration paramters |
|
50 GetParamFuncs = {} |
|
51 SetParamFuncs = {} |
|
52 |
|
53 |
|
54 # Upon PLC load, this Dictionary is initialised with the BACnet configuration |
|
55 # hardcoded in the C file |
|
56 # (i.e. the configuration inserted in Beremiz IDE when project was compiled) |
|
57 _DefaultConfiguration = None |
|
58 |
|
59 |
|
60 # Dictionary that contains the BACnet configuration currently being shown |
|
61 # on the web interface |
|
62 # This configuration will almost always be identical to the current |
|
63 # configuration in the PLC (i.e., the current state stored in the |
|
64 # C variables in the .so file). |
|
65 # The configuration viewed on the web will only be different to the current |
|
66 # configuration when the user edits the configuration, and when |
|
67 # the user asks to save the edited configuration but it contains an error. |
|
68 _WebviewConfiguration = None |
|
69 |
|
70 |
|
71 # Dictionary that stores the BACnet configuration currently stored in a file |
|
72 # Currently only used to decide whether or not to show the "Delete" button on the |
|
73 # web interface (only shown if _SavedConfiguration is not None) |
|
74 _SavedConfiguration = None |
|
75 |
|
76 |
|
77 # File to which the new BACnet configuration gets stored on the PLC |
|
78 # Note that the stored configuration is likely different to the |
|
79 # configuration hardcoded in C generated code (.so file), so |
|
80 # this file should be persistent across PLC reboots so we can |
|
81 # re-configure the PLC (change values of variables in .so file) |
|
82 # before it gets a chance to start running |
|
83 # |
|
84 #_BACnetConfFilename = None |
|
85 _BACnetConfFilename = "/tmp/BeremizBACnetConfig.json" |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 class BN_StrippedString(annotate.String): |
|
91 def __init__(self, *args, **kwargs): |
|
92 annotate.String.__init__(self, strip = True, *args, **kwargs) |
|
93 |
|
94 |
|
95 |
|
96 BACnet_parameters = [ |
|
97 # param. name label ctype type annotate type |
|
98 # (C code var name) (used on web interface) (C data type) (web data type) |
|
99 # (annotate.String, |
|
100 # annotate.Integer, ...) |
|
101 ("network_interface" , _("Network Interface") , ctypes.c_char_p, BN_StrippedString), |
|
102 ("port_number" , _("UDP Port Number") , ctypes.c_char_p, BN_StrippedString), |
|
103 ("comm_control_passwd" , _("BACnet Communication Control Password") , ctypes.c_char_p, annotate.String), |
|
104 ("device_id" , _("BACnet Device ID") , ctypes.c_int, annotate.Integer), |
|
105 ("device_name" , _("BACnet Device Name") , ctypes.c_char_p, annotate.String), |
|
106 ("device_location" , _("BACnet Device Location") , ctypes.c_char_p, annotate.String), |
|
107 ("device_description" , _("BACnet Device Description") , ctypes.c_char_p, annotate.String), |
|
108 ("device_appsoftware_ver" , _("BACnet Device Application Software Version"), ctypes.c_char_p, annotate.String) |
|
109 ] |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 def _CheckPortnumber(port_number): |
|
117 """ check validity of the port number """ |
|
118 try: |
|
119 portnum = int(port_number) |
|
120 if (portnum < 0) or (portnum > 65535): |
|
121 raise Exception |
|
122 except Exception: |
|
123 return False |
|
124 |
|
125 return True |
|
126 |
|
127 |
|
128 |
|
129 def _CheckDeviceID(device_id): |
|
130 """ |
|
131 # check validity of the Device ID |
|
132 # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID) |
|
133 # so the Device instance ID is limited from 0 to 22^2-1 = 4194303 |
|
134 # However, 4194303 is reserved for special use (similar to NULL pointer), so last |
|
135 # valid ID becomes 4194302 |
|
136 """ |
|
137 try: |
|
138 devid = int(device_id) |
|
139 if (devid < 0) or (devid > 4194302): |
|
140 raise Exception |
|
141 except Exception: |
|
142 return False |
|
143 |
|
144 return True |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 def _CheckConfiguration(BACnetConfig): |
|
151 res = True |
|
152 res = res and _CheckPortnumber(BACnetConfig["port_number"]) |
|
153 res = res and _CheckDeviceID (BACnetConfig["device_id"]) |
|
154 return res |
|
155 |
|
156 |
|
157 |
|
158 def _CheckWebConfiguration(BACnetConfig): |
|
159 res = True |
|
160 |
|
161 # check the port number |
|
162 if not _CheckPortnumber(BACnetConfig["port_number"]): |
|
163 raise annotate.ValidateError( |
|
164 {"port_number": "Invalid port number: " + str(BACnetConfig["port_number"])}, |
|
165 _("BACnet configuration error:")) |
|
166 res = False |
|
167 |
|
168 if not _CheckDeviceID(BACnetConfig["device_id"]): |
|
169 raise annotate.ValidateError( |
|
170 {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])}, |
|
171 _("BACnet configuration error:")) |
|
172 res = False |
|
173 |
|
174 return res |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 def _SetSavedConfiguration(BACnetConfig): |
|
182 """ Stores in a file a dictionary containing the BACnet parameter configuration """ |
|
183 with open(os.path.realpath(_BACnetConfFilename), 'w') as f: |
|
184 json.dump(BACnetConfig, f, sort_keys=True, indent=4) |
|
185 global _SavedConfiguration |
|
186 _SavedConfiguration = BACnetConfig |
|
187 |
|
188 |
|
189 def _DelSavedConfiguration(): |
|
190 """ Deletes the file cotaining the persistent BACnet configuration """ |
|
191 if os.path.exists(_BACnetConfFilename): |
|
192 os.remove(_BACnetConfFilename) |
|
193 |
|
194 |
|
195 def _GetSavedConfiguration(): |
|
196 """ |
|
197 # Returns a dictionary containing the BACnet parameter configuration |
|
198 # that was last saved to file. If no file exists, then return None |
|
199 """ |
|
200 try: |
|
201 #if os.path.isfile(_BACnetConfFilename): |
|
202 saved_config = json.load(open(_BACnetConfFilename)) |
|
203 except Exception: |
|
204 return None |
|
205 |
|
206 if _CheckConfiguration(saved_config): |
|
207 return saved_config |
|
208 else: |
|
209 return None |
|
210 |
|
211 |
|
212 def _GetPLCConfiguration(): |
|
213 """ |
|
214 # Returns a dictionary containing the current BACnet parameter configuration |
|
215 # stored in the C variables in the loaded PLC (.so file) |
|
216 """ |
|
217 current_config = {} |
|
218 for par_name, x1, x2, x3 in BACnet_parameters: |
|
219 value = GetParamFuncs[par_name]() |
|
220 if value is not None: |
|
221 current_config[par_name] = value |
|
222 |
|
223 return current_config |
|
224 |
|
225 |
|
226 def _SetPLCConfiguration(BACnetConfig): |
|
227 """ |
|
228 # Stores the BACnet parameter configuration into the |
|
229 # the C variables in the loaded PLC (.so file) |
|
230 """ |
|
231 for par_name in BACnetConfig: |
|
232 value = BACnetConfig[par_name] |
|
233 #_plcobj.LogMessage("BACnet web server extension::_SetPLCConfiguration() Setting " |
|
234 # + par_name + " to " + str(value) ) |
|
235 if value is not None: |
|
236 SetParamFuncs[par_name](value) |
|
237 # update the configuration shown on the web interface |
|
238 global _WebviewConfiguration |
|
239 _WebviewConfiguration = _GetPLCConfiguration() |
|
240 |
|
241 |
|
242 |
|
243 def _GetWebviewConfigurationValue(ctx, argument): |
|
244 """ |
|
245 # Callback function, called by the web interface (NevowServer.py) |
|
246 # to fill in the default value of each parameter |
|
247 """ |
|
248 try: |
|
249 return _WebviewConfiguration[argument.name] |
|
250 except Exception: |
|
251 return "" |
|
252 |
|
253 |
|
254 # The configuration of the web form used to see/edit the BACnet parameters |
|
255 webFormInterface = [(name, web_dtype (label=web_label, default=_GetWebviewConfigurationValue)) |
|
256 for name, web_label, c_dtype, web_dtype in BACnet_parameters] |
|
257 |
|
258 |
|
259 |
|
260 def _updateWebInterface(): |
|
261 """ |
|
262 # Add/Remove buttons to/from the web interface depending on the current state |
|
263 # |
|
264 # - If there is a saved state => add a delete saved state button |
|
265 """ |
|
266 |
|
267 # Add a "Delete Saved Configuration" button if there is a saved configuration! |
|
268 if _SavedConfiguration is None: |
|
269 _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved") |
|
270 else: |
|
271 _NS.ConfigurableSettings.addSettings( |
|
272 "BACnetConfigDelSaved", # name |
|
273 _("BACnet Configuration"), # description |
|
274 [], # fields (empty, no parameters required!) |
|
275 _("Delete Configuration Stored in Persistent Storage"), # button label |
|
276 OnButtonDel, # callback |
|
277 "BACnetConfigParm") # Add after entry xxxx |
|
278 |
|
279 |
|
280 def OnButtonSave(**kwargs): |
|
281 """ |
|
282 # Function called when user clicks 'Save' button in web interface |
|
283 # The function will configure the BACnet plugin in the PLC with the values |
|
284 # specified in the web interface. However, values must be validated first! |
|
285 """ |
|
286 |
|
287 #_plcobj.LogMessage("BACnet web server extension::OnButtonSave() Called") |
|
288 |
|
289 newConfig = {} |
|
290 for par_name, x1, x2, x3 in BACnet_parameters: |
|
291 value = kwargs.get(par_name, None) |
|
292 if value is not None: |
|
293 newConfig[par_name] = value |
|
294 |
|
295 global _WebviewConfiguration |
|
296 _WebviewConfiguration = newConfig |
|
297 |
|
298 # First check if configuration is OK. |
|
299 if not _CheckWebConfiguration(newConfig): |
|
300 return |
|
301 |
|
302 # store to file the new configuration so that |
|
303 # we can recoup the configuration the next time the PLC |
|
304 # has a cold start (i.e. when Beremiz_service.py is retarted) |
|
305 _SetSavedConfiguration(newConfig) |
|
306 |
|
307 # Configure PLC with the current BACnet parameters |
|
308 _SetPLCConfiguration(newConfig) |
|
309 |
|
310 # File has just been created => Delete button must be shown on web interface! |
|
311 _updateWebInterface() |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 def OnButtonDel(**kwargs): |
|
317 """ |
|
318 # Function called when user clicks 'Delete' button in web interface |
|
319 # The function will delete the file containing the persistent |
|
320 # BACnet configution |
|
321 """ |
|
322 |
|
323 # Delete the file |
|
324 _DelSavedConfiguration() |
|
325 # Set the current configuration to the default (hardcoded in C) |
|
326 _SetPLCConfiguration(_DefaultConfiguration) |
|
327 # Reset global variable |
|
328 global _SavedConfiguration |
|
329 _SavedConfiguration = None |
|
330 # File has just been deleted => Delete button on web interface no longer needed! |
|
331 _updateWebInterface() |
|
332 |
|
333 |
|
334 |
|
335 def OnButtonShowCur(**kwargs): |
|
336 """ |
|
337 # Function called when user clicks 'Show Current PLC Configuration' button in web interface |
|
338 # The function will load the current PLC configuration into the web form |
|
339 """ |
|
340 |
|
341 global _WebviewConfiguration |
|
342 _WebviewConfiguration = _GetPLCConfiguration() |
|
343 # File has just been deleted => Delete button on web interface no longer needed! |
|
344 _updateWebInterface() |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 def OnLoadPLC(): |
|
350 """ |
|
351 # Callback function, called (by PLCObject.py) when a new PLC program |
|
352 # (i.e. XXX.so file) is transfered to the PLC runtime |
|
353 # and oaded into memory |
|
354 """ |
|
355 |
|
356 #_plcobj.LogMessage("BACnet web server extension::OnLoadPLC() Called...") |
|
357 |
|
358 if _plcobj.PLClibraryHandle is None: |
|
359 # PLC was loaded but we don't have access to the library of compiled code (.so lib)? |
|
360 # Hmm... This shold never occur!! |
|
361 return |
|
362 |
|
363 # Get the location (in the Config. Node Tree of Beremiz IDE) the BACnet plugin |
|
364 # occupies in the currently loaded PLC project (i.e., the .so file) |
|
365 # If the "__bacnet_plugin_location" C variable is not present in the .so file, |
|
366 # we conclude that the currently loaded PLC does not have the BACnet plugin |
|
367 # included (situation (2b) described above init()) |
|
368 try: |
|
369 location = ctypes.c_char_p.in_dll(_plcobj.PLClibraryHandle, "__bacnet_plugin_location") |
|
370 except Exception: |
|
371 # Loaded PLC does not have the BACnet plugin => nothing to do |
|
372 # (i.e. do _not_ configure and make available the BACnet web interface) |
|
373 return |
|
374 |
|
375 # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters |
|
376 for name, web_label, c_dtype, web_dtype in BACnet_parameters: |
|
377 GetParamFuncName = "__bacnet_" + location.value + "_get_ConfigParam_" + name |
|
378 SetParamFuncName = "__bacnet_" + location.value + "_set_ConfigParam_" + name |
|
379 |
|
380 GetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, GetParamFuncName) |
|
381 GetParamFuncs[name].restype = c_dtype |
|
382 GetParamFuncs[name].argtypes = None |
|
383 |
|
384 SetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, SetParamFuncName) |
|
385 SetParamFuncs[name].restype = None |
|
386 SetParamFuncs[name].argtypes = [c_dtype] |
|
387 |
|
388 # Default configuration is the configuration done in Beremiz IDE |
|
389 # whose parameters get hardcoded into C, and compiled into the .so file |
|
390 # We read the default configuration from the .so file before the values |
|
391 # get changed by the user using the web server, or by the call (further on) |
|
392 # to _SetPLCConfiguration(SavedConfiguration) |
|
393 global _DefaultConfiguration |
|
394 _DefaultConfiguration = _GetPLCConfiguration() |
|
395 |
|
396 # Show the current PLC configuration on the web interface |
|
397 global _WebviewConfiguration |
|
398 _WebviewConfiguration = _GetPLCConfiguration() |
|
399 |
|
400 # Read from file the last used configuration, which is likely |
|
401 # different to the hardcoded configuration. |
|
402 # We Reset the current configuration (i.e., the config stored in the |
|
403 # variables of .so file) to this saved configuration |
|
404 # so the PLC will start off with this saved configuration instead |
|
405 # of the hardcoded (in Beremiz C generated code) configuration values. |
|
406 # |
|
407 # Note that _SetPLCConfiguration() will also update |
|
408 # _WebviewConfiguration , if necessary. |
|
409 global _SavedConfiguration |
|
410 _SavedConfiguration = _GetSavedConfiguration() |
|
411 if _SavedConfiguration is not None: |
|
412 if _CheckConfiguration(_SavedConfiguration): |
|
413 _SetPLCConfiguration(_SavedConfiguration) |
|
414 |
|
415 # Configure the web interface to include the BACnet config parameters |
|
416 _NS.ConfigurableSettings.addSettings( |
|
417 "BACnetConfigParm", # name |
|
418 _("BACnet Configuration"), # description |
|
419 webFormInterface, # fields |
|
420 _("Save Configuration to Persistent Storage"), # button label |
|
421 OnButtonSave) # callback |
|
422 |
|
423 # Add a "View Current Configuration" button |
|
424 _NS.ConfigurableSettings.addSettings( |
|
425 "BACnetConfigViewCur", # name |
|
426 _("BACnet Configuration"), # description |
|
427 [], # fields (empty, no parameters required!) |
|
428 _("Show Current PLC Configuration"), # button label |
|
429 OnButtonShowCur) # callback |
|
430 |
|
431 # Add the Delete button to the web interface, if required |
|
432 _updateWebInterface() |
|
433 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 def OnUnLoadPLC(): |
|
439 """ |
|
440 # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory |
|
441 """ |
|
442 |
|
443 #_plcobj.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...") |
|
444 |
|
445 # Delete the BACnet specific web interface extensions |
|
446 # (Safe to ask to delete, even if it has not been added!) |
|
447 _NS.ConfigurableSettings.delSettings("BACnetConfigParm") |
|
448 _NS.ConfigurableSettings.delSettings("BACnetConfigViewCur") |
|
449 _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved") |
|
450 GetParamFuncs = {} |
|
451 SetParamFuncs = {} |
|
452 _WebviewConfiguration = None |
|
453 _SavedConfiguration = None |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 # The Beremiz_service.py service, along with the integrated web server it launches |
|
459 # (i.e. Nevow web server, in runtime/NevowServer.py), will go through several states |
|
460 # once started: |
|
461 # (1) Web server is started, but no PLC is loaded |
|
462 # (2) PLC is loaded (i.e. the PLC compiled code is loaded) |
|
463 # (a) The loaded PLC includes the BACnet plugin |
|
464 # (b) The loaded PLC does not have the BACnet plugin |
|
465 # |
|
466 # During (1) and (2a): |
|
467 # we configure the web server interface to not have the BACnet web configuration extension |
|
468 # During (2b) |
|
469 # we configure the web server interface to include the BACnet web configuration extension |
|
470 # |
|
471 # plcobj : reference to the PLCObject defined in PLCObject.py |
|
472 # NS : reference to the web server (i.e. the NevowServer.py module) |
|
473 # WorkingDir: the directory on which Beremiz_service.py is running, and where |
|
474 # all the files downloaded to the PLC get stored, including |
|
475 # the .so file with the compiled C generated code |
|
476 def init(plcobj, NS, WorkingDir): |
|
477 #plcobj.LogMessage("BACnet web server extension::init(plcobj, NS, " + WorkingDir + ") Called") |
|
478 global _WorkingDir |
|
479 _WorkingDir = WorkingDir |
|
480 global _plcobj |
|
481 _plcobj = plcobj |
|
482 global _NS |
|
483 _NS = NS |
|
484 global _BACnetConfFilename |
|
485 if _BACnetConfFilename is None: |
|
486 _BACnetConfFilename = os.path.join(WorkingDir, "BACnetConfig.json") |
|
487 |
|
488 _plcobj.RegisterCallbackLoad ("BACnet_Settins_Extension", OnLoadPLC) |
|
489 _plcobj.RegisterCallbackUnLoad("BACnet_Settins_Extension", OnUnLoadPLC) |
|
490 OnUnLoadPLC() # init is called before the PLC gets loaded... so we make sure we have the correct state |
|