msousa@2649: #!/usr/bin/env python msousa@2649: # -*- coding: utf-8 -*- msousa@2649: msousa@2649: # This file is part of Beremiz runtime. msousa@2649: # msousa@2649: # Copyright (C) 2020: Mario de Sousa msousa@2649: # msousa@2649: # See COPYING.Runtime file for copyrights details. msousa@2649: # msousa@2649: # This library is free software; you can redistribute it and/or msousa@2649: # modify it under the terms of the GNU Lesser General Public msousa@2649: # License as published by the Free Software Foundation; either msousa@2649: # version 2.1 of the License, or (at your option) any later version. msousa@2649: msousa@2649: # This library is distributed in the hope that it will be useful, msousa@2649: # but WITHOUT ANY WARRANTY; without even the implied warranty of msousa@2649: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU msousa@2649: # Lesser General Public License for more details. msousa@2649: msousa@2649: # You should have received a copy of the GNU Lesser General Public msousa@2649: # License along with this library; if not, write to the Free Software msousa@2649: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA msousa@2649: msousa@2649: msousa@2649: import json msousa@2649: import os msousa@2649: import ctypes msousa@2649: msousa@2649: from formless import annotate, webform msousa@2649: msousa@2649: msousa@2649: msousa@2649: # reference to the PLCObject in runtime/PLCObject.py msousa@2649: # PLCObject is a singleton, created in runtime/__init__.py msousa@2649: _plcobj = None msousa@2649: msousa@2649: # reference to the Nevow web server (a.k.a as NS in Beremiz_service.py) msousa@2649: # (Note that NS will reference the NevowServer.py _module_, and not an object/class) msousa@2649: _NS = None msousa@2649: msousa@2649: msousa@2649: # WorkingDir: the directory on which Beremiz_service.py is running, and where msousa@2649: # all the files downloaded to the PLC get stored msousa@2649: _WorkingDir = None msousa@2649: msousa@2649: msousa@2649: # Will contain references to the C functions msousa@2649: # (implemented in beremiz/bacnet/runtime/server.c) msousa@2649: # used to get/set the BACnet specific configuration paramters msousa@2649: GetParamFuncs = {} msousa@2649: SetParamFuncs = {} msousa@2649: msousa@2649: msousa@2649: # Upon PLC load, this Dictionary is initialised with the BACnet configuration msousa@2649: # hardcoded in the C file msousa@2649: # (i.e. the configuration inserted in Beremiz IDE when project was compiled) msousa@2649: _DefaultConfiguration = None msousa@2649: msousa@2649: msousa@2649: # Dictionary that contains the BACnet configuration currently being shown msousa@2649: # on the web interface msousa@2649: # This configuration will almost always be identical to the current msousa@2649: # configuration in the PLC (i.e., the current state stored in the msousa@2649: # C variables in the .so file). msousa@2649: # The configuration viewed on the web will only be different to the current msousa@2649: # configuration when the user edits the configuration, and when msousa@2649: # the user asks to save the edited configuration but it contains an error. msousa@2649: _WebviewConfiguration = None msousa@2649: msousa@2649: msousa@2649: # Dictionary that stores the BACnet configuration currently stored in a file msousa@2649: # Currently only used to decide whether or not to show the "Delete" button on the msousa@2649: # web interface (only shown if _SavedConfiguration is not None) msousa@2649: _SavedConfiguration = None msousa@2649: msousa@2649: msousa@2649: # File to which the new BACnet configuration gets stored on the PLC msousa@2649: # Note that the stored configuration is likely different to the msousa@2649: # configuration hardcoded in C generated code (.so file), so msousa@2649: # this file should be persistent across PLC reboots so we can msousa@2649: # re-configure the PLC (change values of variables in .so file) msousa@2649: # before it gets a chance to start running msousa@2649: # msousa@2649: #_BACnetConfFilename = None msousa@2649: _BACnetConfFilename = "/tmp/BeremizBACnetConfig.json" msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: BACnet_parameters = [ msousa@2649: # param. name label ctype type annotate type msousa@2649: # (C code var name) (used on web interface) (C data type) (web data type) msousa@2649: # (annotate.String, msousa@2649: # annotate.Integer, ...) msousa@2649: ("network_interface" , _("Network Interface") , ctypes.c_char_p, annotate.String), msousa@2649: ("port_number" , _("UDP Port Number") , ctypes.c_char_p, annotate.String), msousa@2649: ("comm_control_passwd" , _("BACnet Communication Control Password") , ctypes.c_char_p, annotate.String), msousa@2649: ("device_id" , _("BACnet Device ID") , ctypes.c_int, annotate.Integer), msousa@2649: ("device_name" , _("BACnet Device Name") , ctypes.c_char_p, annotate.String), msousa@2649: ("device_location" , _("BACnet Device Location") , ctypes.c_char_p, annotate.String), msousa@2649: ("device_description" , _("BACnet Device Description") , ctypes.c_char_p, annotate.String), msousa@2649: ("device_appsoftware_ver" , _("BACnet Device Application Software Version"), ctypes.c_char_p, annotate.String) msousa@2649: ] msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: def _CheckPortnumber(port_number): msousa@2649: """ check validity of the port number """ msousa@2649: try: msousa@2649: portnum = int(port_number) msousa@2649: if (portnum < 0) or (portnum > 65535): msousa@2649: raise Exception msousa@2649: except Exception: msousa@2649: return False msousa@2649: msousa@2649: return True msousa@2649: msousa@2649: msousa@2649: msousa@2649: def _CheckDeviceID(device_id): msousa@2649: """ msousa@2649: # check validity of the Device ID msousa@2649: # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID) msousa@2649: # so the Device instance ID is limited from 0 to 22^2-1 = 4194303 msousa@2649: # However, 4194303 is reserved for special use (similar to NULL pointer), so last msousa@2649: # valid ID becomes 4194302 msousa@2649: """ msousa@2649: try: msousa@2649: devid = int(device_id) msousa@2649: if (devid < 0) or (devid > 4194302): msousa@2649: raise Exception msousa@2649: except Exception: msousa@2649: return False msousa@2649: msousa@2649: return True msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: def _CheckConfiguration(BACnetConfig): msousa@2649: res = True msousa@2649: res = res and _CheckPortnumber(BACnetConfig["port_number"]) msousa@2649: res = res and _CheckDeviceID (BACnetConfig["device_id"]) msousa@2649: return res msousa@2649: msousa@2649: msousa@2649: msousa@2649: def _CheckWebConfiguration(BACnetConfig): msousa@2649: res = True msousa@2649: msousa@2649: # check the port number msousa@2649: if not _CheckPortnumber(BACnetConfig["port_number"]): msousa@2649: raise annotate.ValidateError( msousa@2649: {"port_number": "Invalid port number: " + str(BACnetConfig["port_number"])}, msousa@2649: _("BACnet configuration error:")) msousa@2649: res = False msousa@2649: msousa@2649: if not _CheckDeviceID(BACnetConfig["device_id"]): msousa@2649: raise annotate.ValidateError( msousa@2649: {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])}, msousa@2649: _("BACnet configuration error:")) msousa@2649: res = False msousa@2649: msousa@2649: return res msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: def _SetSavedConfiguration(BACnetConfig): msousa@2649: """ Stores in a file a dictionary containing the BACnet parameter configuration """ msousa@2649: with open(os.path.realpath(_BACnetConfFilename), 'w') as f: msousa@2649: json.dump(BACnetConfig, f, sort_keys=True, indent=4) msousa@2649: global _SavedConfiguration msousa@2649: _SavedConfiguration = BACnetConfig msousa@2649: msousa@2649: msousa@2649: def _DelSavedConfiguration(): msousa@2649: """ Deletes the file cotaining the persistent BACnet configuration """ msousa@2649: if os.path.exists(_BACnetConfFilename): msousa@2649: os.remove(_BACnetConfFilename) msousa@2649: msousa@2649: msousa@2649: def _GetSavedConfiguration(): msousa@2649: """ msousa@2649: # Returns a dictionary containing the BACnet parameter configuration msousa@2649: # that was last saved to file. If no file exists, then return None msousa@2649: """ msousa@2649: try: msousa@2649: #if os.path.isfile(_BACnetConfFilename): msousa@2649: saved_config = json.load(open(_BACnetConfFilename)) msousa@2649: except Exception: msousa@2649: return None msousa@2649: msousa@2649: if _CheckConfiguration(saved_config): msousa@2649: return saved_config msousa@2649: else: msousa@2649: return None msousa@2649: msousa@2649: msousa@2649: def _GetPLCConfiguration(): msousa@2649: """ msousa@2649: # Returns a dictionary containing the current BACnet parameter configuration msousa@2649: # stored in the C variables in the loaded PLC (.so file) msousa@2649: """ msousa@2649: current_config = {} msousa@2649: for par_name, x1, x2, x3 in BACnet_parameters: msousa@2649: value = GetParamFuncs[par_name]() msousa@2649: if value is not None: msousa@2649: current_config[par_name] = value msousa@2649: msousa@2649: return current_config msousa@2649: msousa@2649: msousa@2649: def _SetPLCConfiguration(BACnetConfig): msousa@2649: """ msousa@2649: # Stores the BACnet parameter configuration into the msousa@2649: # the C variables in the loaded PLC (.so file) msousa@2649: """ msousa@2649: for par_name in BACnetConfig: msousa@2649: value = BACnetConfig[par_name] msousa@2649: #_plcobj.LogMessage("BACnet web server extension::_SetPLCConfiguration() Setting " msousa@2649: # + par_name + " to " + str(value) ) msousa@2649: if value is not None: msousa@2649: SetParamFuncs[par_name](value) msousa@2649: # update the configuration shown on the web interface msousa@2649: global _WebviewConfiguration msousa@2649: _WebviewConfiguration = _GetPLCConfiguration() msousa@2649: msousa@2649: msousa@2649: msousa@2649: def _GetWebviewConfigurationValue(ctx, argument): msousa@2649: """ msousa@2649: # Callback function, called by the web interface (NevowServer.py) msousa@2649: # to fill in the default value of each parameter msousa@2649: """ msousa@2649: try: msousa@2649: return _WebviewConfiguration[argument.name] msousa@2649: except Exception: msousa@2649: return "" msousa@2649: msousa@2649: msousa@2649: # The configuration of the web form used to see/edit the BACnet parameters msousa@2649: webFormInterface = [(name, web_dtype (label=web_label, default=_GetWebviewConfigurationValue)) msousa@2649: for name, web_label, c_dtype, web_dtype in BACnet_parameters] msousa@2649: msousa@2649: msousa@2649: msousa@2649: def _updateWebInterface(): msousa@2649: """ msousa@2649: # Add/Remove buttons to/from the web interface depending on the current state msousa@2649: # msousa@2649: # - If there is a saved state => add a delete saved state button msousa@2649: """ msousa@2649: msousa@2649: # Add a "Delete Saved Configuration" button if there is a saved configuration! msousa@2649: if _SavedConfiguration is None: msousa@2649: _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved") msousa@2649: else: msousa@2649: _NS.ConfigurableSettings.addSettings( msousa@2649: "BACnetConfigDelSaved", # name msousa@2649: _("BACnet Configuration"), # description msousa@2649: [], # fields (empty, no parameters required!) msousa@2649: _("Delete Configuration Stored in Persistent Storage"), # button label msousa@2649: OnButtonDel) # callback msousa@2649: msousa@2649: msousa@2649: msousa@2649: def OnButtonSave(**kwargs): msousa@2649: """ msousa@2649: # Function called when user clicks 'Save' button in web interface msousa@2649: # The function will configure the BACnet plugin in the PLC with the values msousa@2649: # specified in the web interface. However, values must be validated first! msousa@2649: """ msousa@2649: msousa@2649: #_plcobj.LogMessage("BACnet web server extension::OnButtonSave() Called") msousa@2649: msousa@2649: newConfig = {} msousa@2649: for par_name, x1, x2, x3 in BACnet_parameters: msousa@2649: value = kwargs.get(par_name, None) msousa@2649: if value is not None: msousa@2649: newConfig[par_name] = value msousa@2649: msousa@2649: global _WebviewConfiguration msousa@2649: _WebviewConfiguration = newConfig msousa@2649: msousa@2649: # First check if configuration is OK. msousa@2649: if not _CheckWebConfiguration(newConfig): msousa@2649: return msousa@2649: msousa@2649: # store to file the new configuration so that msousa@2649: # we can recoup the configuration the next time the PLC msousa@2649: # has a cold start (i.e. when Beremiz_service.py is retarted) msousa@2649: _SetSavedConfiguration(newConfig) msousa@2649: msousa@2649: # Configure PLC with the current BACnet parameters msousa@2649: _SetPLCConfiguration(newConfig) msousa@2649: msousa@2649: # File has just been created => Delete button must be shown on web interface! msousa@2649: _updateWebInterface() msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: def OnButtonDel(**kwargs): msousa@2649: """ msousa@2649: # Function called when user clicks 'Delete' button in web interface msousa@2649: # The function will delete the file containing the persistent msousa@2649: # BACnet configution msousa@2649: """ msousa@2649: msousa@2649: # Delete the file msousa@2649: _DelSavedConfiguration() msousa@2649: # Set the current configuration to the default (hardcoded in C) msousa@2649: _SetPLCConfiguration(_DefaultConfiguration) msousa@2649: # Reset global variable msousa@2649: global _SavedConfiguration msousa@2649: _SavedConfiguration = None msousa@2649: # File has just been deleted => Delete button on web interface no longer needed! msousa@2649: _updateWebInterface() msousa@2649: msousa@2649: msousa@2649: msousa@2649: def OnButtonShowCur(**kwargs): msousa@2649: """ msousa@2649: # Function called when user clicks 'Show Current PLC Configuration' button in web interface msousa@2649: # The function will load the current PLC configuration into the web form msousa@2649: """ msousa@2649: msousa@2649: global _WebviewConfiguration msousa@2649: _WebviewConfiguration = _GetPLCConfiguration() msousa@2649: # File has just been deleted => Delete button on web interface no longer needed! msousa@2649: _updateWebInterface() msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: def OnLoadPLC(): msousa@2649: """ msousa@2649: # Callback function, called (by PLCObject.py) when a new PLC program msousa@2649: # (i.e. XXX.so file) is transfered to the PLC runtime msousa@2649: # and oaded into memory msousa@2649: """ msousa@2649: msousa@2649: #_plcobj.LogMessage("BACnet web server extension::OnLoadPLC() Called...") msousa@2649: msousa@2649: if _plcobj.PLClibraryHandle is None: msousa@2649: # PLC was loaded but we don't have access to the library of compiled code (.so lib)? msousa@2649: # Hmm... This shold never occur!! msousa@2649: return msousa@2649: msousa@2649: # Get the location (in the Config. Node Tree of Beremiz IDE) the BACnet plugin msousa@2649: # occupies in the currently loaded PLC project (i.e., the .so file) msousa@2649: # If the "__bacnet_plugin_location" C variable is not present in the .so file, msousa@2649: # we conclude that the currently loaded PLC does not have the BACnet plugin msousa@2649: # included (situation (2b) described above init()) msousa@2649: try: msousa@2649: location = ctypes.c_char_p.in_dll(_plcobj.PLClibraryHandle, "__bacnet_plugin_location") msousa@2649: except Exception: msousa@2649: # Loaded PLC does not have the BACnet plugin => nothing to do msousa@2649: # (i.e. do _not_ configure and make available the BACnet web interface) msousa@2649: return msousa@2649: msousa@2649: # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters msousa@2649: for name, web_label, c_dtype, web_dtype in BACnet_parameters: msousa@2649: GetParamFuncName = "__bacnet_" + location.value + "_get_ConfigParam_" + name msousa@2649: SetParamFuncName = "__bacnet_" + location.value + "_set_ConfigParam_" + name msousa@2649: msousa@2649: GetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, GetParamFuncName) msousa@2649: GetParamFuncs[name].restype = c_dtype msousa@2649: GetParamFuncs[name].argtypes = None msousa@2649: msousa@2649: SetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, SetParamFuncName) msousa@2649: SetParamFuncs[name].restype = None msousa@2649: SetParamFuncs[name].argtypes = [c_dtype] msousa@2649: msousa@2649: # Default configuration is the configuration done in Beremiz IDE msousa@2649: # whose parameters get hardcoded into C, and compiled into the .so file msousa@2649: # We read the default configuration from the .so file before the values msousa@2649: # get changed by the user using the web server, or by the call (further on) msousa@2649: # to _SetPLCConfiguration(SavedConfiguration) msousa@2649: global _DefaultConfiguration msousa@2649: _DefaultConfiguration = _GetPLCConfiguration() msousa@2649: msousa@2649: # Show the current PLC configuration on the web interface msousa@2649: global _WebviewConfiguration msousa@2649: _WebviewConfiguration = _GetPLCConfiguration() msousa@2649: msousa@2649: # Read from file the last used configuration, which is likely msousa@2649: # different to the hardcoded configuration. msousa@2649: # We Reset the current configuration (i.e., the config stored in the msousa@2649: # variables of .so file) to this saved configuration msousa@2649: # so the PLC will start off with this saved configuration instead msousa@2649: # of the hardcoded (in Beremiz C generated code) configuration values. msousa@2649: # msousa@2649: # Note that _SetPLCConfiguration() will also update msousa@2649: # _WebviewConfiguration , if necessary. msousa@2649: global _SavedConfiguration msousa@2649: _SavedConfiguration = _GetSavedConfiguration() msousa@2649: if _SavedConfiguration is not None: msousa@2649: if _CheckConfiguration(_SavedConfiguration): msousa@2649: _SetPLCConfiguration(_SavedConfiguration) msousa@2649: msousa@2649: # Configure the web interface to include the BACnet config parameters msousa@2649: _NS.ConfigurableSettings.addSettings( msousa@2649: "BACnetConfigParm", # name msousa@2649: _("BACnet Configuration"), # description msousa@2649: webFormInterface, # fields msousa@2649: _("Save Configuration to Persistent Storage"), # button label msousa@2649: OnButtonSave) # callback msousa@2649: msousa@2649: # Add a "View Current Configuration" button msousa@2649: _NS.ConfigurableSettings.addSettings( msousa@2649: "BACnetConfigViewCur", # name msousa@2649: _("BACnet Configuration"), # description msousa@2649: [], # fields (empty, no parameters required!) msousa@2649: _("Show Current PLC Configuration"), # button label msousa@2649: OnButtonShowCur) # callback msousa@2649: msousa@2649: # Add the Delete button to the web interface, if required msousa@2649: _updateWebInterface() msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: def OnUnLoadPLC(): msousa@2649: """ msousa@2649: # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory msousa@2649: """ msousa@2649: msousa@2649: #_plcobj.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...") msousa@2649: msousa@2649: # Delete the BACnet specific web interface extensions msousa@2649: # (Safe to ask to delete, even if it has not been added!) msousa@2649: _NS.ConfigurableSettings.delSettings("BACnetConfigParm") msousa@2649: _NS.ConfigurableSettings.delSettings("BACnetConfigViewCur") msousa@2649: _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved") msousa@2649: GetParamFuncs = {} msousa@2649: SetParamFuncs = {} msousa@2649: _WebviewConfiguration = None msousa@2649: _SavedConfiguration = None msousa@2649: msousa@2649: msousa@2649: msousa@2649: msousa@2649: # The Beremiz_service.py service, along with the integrated web server it launches msousa@2649: # (i.e. Nevow web server, in runtime/NevowServer.py), will go through several states msousa@2649: # once started: msousa@2649: # (1) Web server is started, but no PLC is loaded msousa@2649: # (2) PLC is loaded (i.e. the PLC compiled code is loaded) msousa@2649: # (a) The loaded PLC includes the BACnet plugin msousa@2649: # (b) The loaded PLC does not have the BACnet plugin msousa@2649: # msousa@2649: # During (1) and (2a): msousa@2649: # we configure the web server interface to not have the BACnet web configuration extension msousa@2649: # During (2b) msousa@2649: # we configure the web server interface to include the BACnet web configuration extension msousa@2649: # msousa@2649: # plcobj : reference to the PLCObject defined in PLCObject.py msousa@2649: # NS : reference to the web server (i.e. the NevowServer.py module) msousa@2649: # WorkingDir: the directory on which Beremiz_service.py is running, and where msousa@2649: # all the files downloaded to the PLC get stored, including msousa@2649: # the .so file with the compiled C generated code msousa@2649: def init(plcobj, NS, WorkingDir): msousa@2649: #plcobj.LogMessage("BACnet web server extension::init(plcobj, NS, " + WorkingDir + ") Called") msousa@2649: global _WorkingDir msousa@2649: _WorkingDir = WorkingDir msousa@2649: global _plcobj msousa@2649: _plcobj = plcobj msousa@2649: global _NS msousa@2649: _NS = NS msousa@2649: global _BACnetConfFilename msousa@2649: if _BACnetConfFilename is None: msousa@2649: _BACnetConfFilename = os.path.join(WorkingDir, "BACnetConfig.json") msousa@2649: msousa@2649: _plcobj.RegisterCallbackLoad ("BACnet_Settins_Extension", OnLoadPLC) msousa@2649: _plcobj.RegisterCallbackUnLoad("BACnet_Settins_Extension", OnUnLoadPLC) msousa@2649: OnUnLoadPLC() # init is called before the PLC gets loaded... so we make sure we have the correct state