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: Edouard@2669: import runtime.NevowServer as NS 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 Edouard@2686: BacnetGetParamFuncs = {} Edouard@2686: BacnetSetParamFuncs = {} 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) Edouard@2686: _BacnetDefaultConfiguration = 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. Edouard@2686: _BacnetWebviewConfiguration = 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 Edouard@2686: # web interface (only shown if _BacnetSavedConfiguration is not None) Edouard@2686: _BacnetSavedConfiguration = 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 Edouard@2673: _BACnetConfFilename = os.path.join(WorkingDir, "bacnetconf.json") msousa@2649: msousa@2649: msousa@2649: msousa@2667: class BN_StrippedString(annotate.String): msousa@2667: def __init__(self, *args, **kwargs): msousa@2667: annotate.String.__init__(self, strip = True, *args, **kwargs) msousa@2667: 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@2667: ("network_interface" , _("Network Interface") , ctypes.c_char_p, BN_StrippedString), msousa@2667: ("port_number" , _("UDP Port Number") , ctypes.c_char_p, BN_StrippedString), 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: Edouard@2686: def _CheckBacnetPortnumber(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: Edouard@2686: def _CheckBacnetDeviceID(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: Edouard@2686: def _CheckBacnetConfiguration(BACnetConfig): msousa@2649: res = True Edouard@2686: res = res and _CheckBacnetPortnumber(BACnetConfig["port_number"]) Edouard@2686: res = res and _CheckBacnetDeviceID (BACnetConfig["device_id"]) msousa@2649: return res msousa@2649: msousa@2649: msousa@2649: Edouard@2686: def _CheckBacnetWebConfiguration(BACnetConfig): msousa@2649: res = True msousa@2649: msousa@2649: # check the port number Edouard@2686: if not _CheckBacnetPortnumber(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: Edouard@2686: if not _CheckBacnetDeviceID(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: Edouard@2686: def _SetBacnetSavedConfiguration(BACnetConfig): msousa@2649: """ Stores in a file a dictionary containing the BACnet parameter configuration """ Edouard@2686: global _BacnetSavedConfiguration Edouard@2686: Edouard@2686: if BACnetConfig == _BacnetDefaultConfiguration : Edouard@2686: _DelBacnetSavedConfiguration() Edouard@2686: _BacnetSavedConfiguration = None Edouard@2674: else : Edouard@2674: with open(os.path.realpath(_BACnetConfFilename), 'w') as f: Edouard@2674: json.dump(BACnetConfig, f, sort_keys=True, indent=4) Edouard@2686: _BacnetSavedConfiguration = BACnetConfig Edouard@2686: Edouard@2686: Edouard@2686: def _DelBacnetSavedConfiguration(): 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: Edouard@2686: def _GetBacnetSavedConfiguration(): 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: Edouard@2686: if _CheckBacnetConfiguration(saved_config): msousa@2649: return saved_config msousa@2649: else: msousa@2649: return None msousa@2649: msousa@2649: Edouard@2686: def _GetBacnetPLCConfiguration(): 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: Edouard@2686: value = BacnetGetParamFuncs[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: Edouard@2686: def _SetBacnetPLCConfiguration(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] Edouard@2686: #PLCObject.LogMessage("BACnet web server extension::_SetBacnetPLCConfiguration() Setting " msousa@2649: # + par_name + " to " + str(value) ) msousa@2649: if value is not None: Edouard@2686: BacnetSetParamFuncs[par_name](value) msousa@2649: # update the configuration shown on the web interface Edouard@2686: global _BacnetWebviewConfiguration Edouard@2686: _BacnetWebviewConfiguration = _GetBacnetPLCConfiguration() Edouard@2686: Edouard@2686: Edouard@2686: Edouard@2686: def _GetBacnetWebviewConfigurationValue(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: Edouard@2686: return _BacnetWebviewConfiguration[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 Edouard@2686: webFormInterface = [(name, web_dtype (label=web_label, default=_GetBacnetWebviewConfigurationValue)) msousa@2649: for name, web_label, c_dtype, web_dtype in BACnet_parameters] msousa@2649: msousa@2649: Edouard@2686: def OnBacnetButtonSave(**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: Edouard@2686: #PLCObject.LogMessage("BACnet web server extension::OnBacnetButtonSave() 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: msousa@2649: # First check if configuration is OK. Edouard@2686: if not _CheckBacnetWebConfiguration(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) Edouard@2686: _SetBacnetSavedConfiguration(newConfig) msousa@2649: msousa@2649: # Configure PLC with the current BACnet parameters Edouard@2686: _SetBacnetPLCConfiguration(newConfig) Edouard@2686: Edouard@2686: Edouard@2686: Edouard@2686: def OnBacnetButtonReset(**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 Edouard@2686: _DelBacnetSavedConfiguration() msousa@2649: # Set the current configuration to the default (hardcoded in C) Edouard@2686: _SetBacnetPLCConfiguration(_BacnetDefaultConfiguration) msousa@2649: # Reset global variable Edouard@2686: global _BacnetSavedConfiguration Edouard@2686: _BacnetSavedConfiguration = None Edouard@2670: Edouard@2670: Edouard@2670: Edouard@2670: # location_str is replaced by extension's value in CTNGenerateC call Edouard@2993: def _runtime_%(location_str)s_bacnet_websettings_init(): 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: Edouard@2669: #PLCObject.LogMessage("BACnet web server extension::OnLoadPLC() Called...") Edouard@2669: Edouard@2669: if PLCObject.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: # 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: Edouard@2670: # location_str is replaced by extension's value in CTNGenerateC call Edouard@2670: GetParamFuncName = "__bacnet_%(location_str)s_get_ConfigParam_" + name Edouard@2670: SetParamFuncName = "__bacnet_%(location_str)s_set_ConfigParam_" + name msousa@2649: Edouard@2676: # XXX TODO : stop reading from PLC .so file. This code is template code Edouard@2676: # that can use modbus extension build data Edouard@2686: BacnetGetParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, GetParamFuncName) Edouard@2686: BacnetGetParamFuncs[name].restype = c_dtype Edouard@2686: BacnetGetParamFuncs[name].argtypes = None msousa@2649: Edouard@2686: BacnetSetParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, SetParamFuncName) Edouard@2686: BacnetSetParamFuncs[name].restype = None Edouard@2686: BacnetSetParamFuncs[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) Edouard@2686: # to _SetBacnetPLCConfiguration(BacnetSavedConfiguration) Edouard@2686: global _BacnetDefaultConfiguration Edouard@2686: _BacnetDefaultConfiguration = _GetBacnetPLCConfiguration() msousa@2649: msousa@2649: # Show the current PLC configuration on the web interface Edouard@2686: global _BacnetWebviewConfiguration Edouard@2686: _BacnetWebviewConfiguration = _GetBacnetPLCConfiguration() 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: # Edouard@2686: # Note that _SetBacnetPLCConfiguration() will also update Edouard@2686: # _BacnetWebviewConfiguration , if necessary. Edouard@2686: global _BacnetSavedConfiguration Edouard@2686: _BacnetSavedConfiguration = _GetBacnetSavedConfiguration() Edouard@2686: if _BacnetSavedConfiguration is not None: Edouard@2686: if _CheckBacnetConfiguration(_BacnetSavedConfiguration): Edouard@2686: _SetBacnetPLCConfiguration(_BacnetSavedConfiguration) msousa@2649: Edouard@2672: WebSettings = NS.newExtensionSetting("BACnet extension", "bacnet_token") Edouard@2670: msousa@2649: # Configure the web interface to include the BACnet config parameters Edouard@2670: WebSettings.addSettings( msousa@2649: "BACnetConfigParm", # name msousa@2649: _("BACnet Configuration"), # description msousa@2649: webFormInterface, # fields Edouard@2670: _("Apply"), # button label Edouard@2686: OnBacnetButtonSave) # callback Edouard@2670: Edouard@2670: # Add the Delete button to the web interface Edouard@2670: WebSettings.addSettings( Edouard@2670: "BACnetConfigDelSaved", # name msousa@2649: _("BACnet Configuration"), # description Edouard@2670: [ ("status", Edouard@2670: annotate.String(label=_("Current state"), Edouard@2670: immutable=True, Edouard@2686: default=lambda *k:getBacnetConfigStatus())), Edouard@2670: ], # fields (empty, no parameters required!) Edouard@2670: _("Reset"), # button label Edouard@2686: OnBacnetButtonReset) Edouard@2686: Edouard@2686: Edouard@2686: Edouard@2686: def getBacnetConfigStatus(): Edouard@2686: if _BacnetWebviewConfiguration == _BacnetDefaultConfiguration : Edouard@2670: return "Unchanged" Edouard@2670: return "Modified" Edouard@2670: Edouard@2670: Edouard@2670: # location_str is replaced by extension's value in CTNGenerateC call Edouard@2993: def _runtime_%(location_str)s_bacnet_websettings_cleanup(): msousa@2649: """ msousa@2649: # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory msousa@2649: """ msousa@2649: Edouard@2669: #PLCObject.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...") msousa@2649: Edouard@2672: NS.removeExtensionSetting("bacnet_token") Edouard@2670: Edouard@2686: BacnetGetParamFuncs = {} Edouard@2686: BacnetSetParamFuncs = {} Edouard@2686: _BacnetWebviewConfiguration = None Edouard@2686: _BacnetSavedConfiguration = None Edouard@2686: Edouard@2686: Edouard@2686: Edouard@2686: