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 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@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: 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] Edouard@2669: #PLCObject.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: 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: Edouard@2669: #PLCObject.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: 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: Edouard@2670: Edouard@2670: Edouard@2670: def OnButtonReset(**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 Edouard@2670: Edouard@2670: Edouard@2670: Edouard@2670: # location_str is replaced by extension's value in CTNGenerateC call Edouard@2669: def _runtime_bacnet_websettings_%(location_str)s_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@2669: GetParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, GetParamFuncName) msousa@2649: GetParamFuncs[name].restype = c_dtype msousa@2649: GetParamFuncs[name].argtypes = None msousa@2649: Edouard@2669: SetParamFuncs[name] = getattr(PLCObject.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: 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 msousa@2649: OnButtonSave) # 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@2670: default=lambda *k:getConfigStatus())), Edouard@2670: ], # fields (empty, no parameters required!) Edouard@2670: _("Reset"), # button label Edouard@2670: OnButtonReset) Edouard@2670: Edouard@2670: Edouard@2670: Edouard@2670: def getConfigStatus(): Edouard@2670: if _WebviewConfiguration == _DefaultConfiguration : 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@2669: def _runtime_bacnet_websettings_%(location_str)s_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: msousa@2649: GetParamFuncs = {} msousa@2649: SetParamFuncs = {} msousa@2649: _WebviewConfiguration = None msousa@2649: _SavedConfiguration = None msousa@2649: msousa@2649: msousa@2649: msousa@2649: