--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bacnet/web_settings.py Thu Jun 18 11:00:26 2020 +0200
@@ -0,0 +1,402 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz runtime.
+#
+# Copyright (C) 2020: Mario de Sousa
+#
+# See COPYING.Runtime file for copyrights details.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+import json
+import os
+import ctypes
+
+from formless import annotate, webform
+
+import runtime.NevowServer as NS
+
+
+# Will contain references to the C functions
+# (implemented in beremiz/bacnet/runtime/server.c)
+# used to get/set the BACnet specific configuration paramters
+GetParamFuncs = {}
+SetParamFuncs = {}
+
+
+# Upon PLC load, this Dictionary is initialised with the BACnet configuration
+# hardcoded in the C file
+# (i.e. the configuration inserted in Beremiz IDE when project was compiled)
+_DefaultConfiguration = None
+
+
+# Dictionary that contains the BACnet configuration currently being shown
+# on the web interface
+# This configuration will almost always be identical to the current
+# configuration in the PLC (i.e., the current state stored in the
+# C variables in the .so file).
+# The configuration viewed on the web will only be different to the current
+# configuration when the user edits the configuration, and when
+# the user asks to save the edited configuration but it contains an error.
+_WebviewConfiguration = None
+
+
+# Dictionary that stores the BACnet configuration currently stored in a file
+# Currently only used to decide whether or not to show the "Delete" button on the
+# web interface (only shown if _SavedConfiguration is not None)
+_SavedConfiguration = None
+
+
+# File to which the new BACnet configuration gets stored on the PLC
+# Note that the stored configuration is likely different to the
+# configuration hardcoded in C generated code (.so file), so
+# this file should be persistent across PLC reboots so we can
+# re-configure the PLC (change values of variables in .so file)
+# before it gets a chance to start running
+#
+#_BACnetConfFilename = None
+_BACnetConfFilename = os.path.join(WorkingDir, "bacnetconf.json")
+
+
+
+class BN_StrippedString(annotate.String):
+ def __init__(self, *args, **kwargs):
+ annotate.String.__init__(self, strip = True, *args, **kwargs)
+
+
+
+BACnet_parameters = [
+ # param. name label ctype type annotate type
+ # (C code var name) (used on web interface) (C data type) (web data type)
+ # (annotate.String,
+ # annotate.Integer, ...)
+ ("network_interface" , _("Network Interface") , ctypes.c_char_p, BN_StrippedString),
+ ("port_number" , _("UDP Port Number") , ctypes.c_char_p, BN_StrippedString),
+ ("comm_control_passwd" , _("BACnet Communication Control Password") , ctypes.c_char_p, annotate.String),
+ ("device_id" , _("BACnet Device ID") , ctypes.c_int, annotate.Integer),
+ ("device_name" , _("BACnet Device Name") , ctypes.c_char_p, annotate.String),
+ ("device_location" , _("BACnet Device Location") , ctypes.c_char_p, annotate.String),
+ ("device_description" , _("BACnet Device Description") , ctypes.c_char_p, annotate.String),
+ ("device_appsoftware_ver" , _("BACnet Device Application Software Version"), ctypes.c_char_p, annotate.String)
+ ]
+
+
+
+
+
+
+def _CheckPortnumber(port_number):
+ """ check validity of the port number """
+ try:
+ portnum = int(port_number)
+ if (portnum < 0) or (portnum > 65535):
+ raise Exception
+ except Exception:
+ return False
+
+ return True
+
+
+
+def _CheckDeviceID(device_id):
+ """
+ # check validity of the Device ID
+ # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID)
+ # so the Device instance ID is limited from 0 to 22^2-1 = 4194303
+ # However, 4194303 is reserved for special use (similar to NULL pointer), so last
+ # valid ID becomes 4194302
+ """
+ try:
+ devid = int(device_id)
+ if (devid < 0) or (devid > 4194302):
+ raise Exception
+ except Exception:
+ return False
+
+ return True
+
+
+
+
+
+def _CheckConfiguration(BACnetConfig):
+ res = True
+ res = res and _CheckPortnumber(BACnetConfig["port_number"])
+ res = res and _CheckDeviceID (BACnetConfig["device_id"])
+ return res
+
+
+
+def _CheckWebConfiguration(BACnetConfig):
+ res = True
+
+ # check the port number
+ if not _CheckPortnumber(BACnetConfig["port_number"]):
+ raise annotate.ValidateError(
+ {"port_number": "Invalid port number: " + str(BACnetConfig["port_number"])},
+ _("BACnet configuration error:"))
+ res = False
+
+ if not _CheckDeviceID(BACnetConfig["device_id"]):
+ raise annotate.ValidateError(
+ {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])},
+ _("BACnet configuration error:"))
+ res = False
+
+ return res
+
+
+
+
+
+
+def _SetSavedConfiguration(BACnetConfig):
+ """ Stores in a file a dictionary containing the BACnet parameter configuration """
+ global _SavedConfiguration
+
+ if BACnetConfig == _DefaultConfiguration :
+ _DelSavedConfiguration()
+ _SavedConfiguration = None
+ else :
+ with open(os.path.realpath(_BACnetConfFilename), 'w') as f:
+ json.dump(BACnetConfig, f, sort_keys=True, indent=4)
+ _SavedConfiguration = BACnetConfig
+
+
+def _DelSavedConfiguration():
+ """ Deletes the file cotaining the persistent BACnet configuration """
+ if os.path.exists(_BACnetConfFilename):
+ os.remove(_BACnetConfFilename)
+
+
+def _GetSavedConfiguration():
+ """
+ # Returns a dictionary containing the BACnet parameter configuration
+ # that was last saved to file. If no file exists, then return None
+ """
+ try:
+ #if os.path.isfile(_BACnetConfFilename):
+ saved_config = json.load(open(_BACnetConfFilename))
+ except Exception:
+ return None
+
+ if _CheckConfiguration(saved_config):
+ return saved_config
+ else:
+ return None
+
+
+def _GetPLCConfiguration():
+ """
+ # Returns a dictionary containing the current BACnet parameter configuration
+ # stored in the C variables in the loaded PLC (.so file)
+ """
+ current_config = {}
+ for par_name, x1, x2, x3 in BACnet_parameters:
+ value = GetParamFuncs[par_name]()
+ if value is not None:
+ current_config[par_name] = value
+
+ return current_config
+
+
+def _SetPLCConfiguration(BACnetConfig):
+ """
+ # Stores the BACnet parameter configuration into the
+ # the C variables in the loaded PLC (.so file)
+ """
+ for par_name in BACnetConfig:
+ value = BACnetConfig[par_name]
+ #PLCObject.LogMessage("BACnet web server extension::_SetPLCConfiguration() Setting "
+ # + par_name + " to " + str(value) )
+ if value is not None:
+ SetParamFuncs[par_name](value)
+ # update the configuration shown on the web interface
+ global _WebviewConfiguration
+ _WebviewConfiguration = _GetPLCConfiguration()
+
+
+
+def _GetWebviewConfigurationValue(ctx, argument):
+ """
+ # Callback function, called by the web interface (NevowServer.py)
+ # to fill in the default value of each parameter
+ """
+ try:
+ return _WebviewConfiguration[argument.name]
+ except Exception:
+ return ""
+
+
+# The configuration of the web form used to see/edit the BACnet parameters
+webFormInterface = [(name, web_dtype (label=web_label, default=_GetWebviewConfigurationValue))
+ for name, web_label, c_dtype, web_dtype in BACnet_parameters]
+
+
+def OnButtonSave(**kwargs):
+ """
+ # Function called when user clicks 'Save' button in web interface
+ # The function will configure the BACnet plugin in the PLC with the values
+ # specified in the web interface. However, values must be validated first!
+ """
+
+ #PLCObject.LogMessage("BACnet web server extension::OnButtonSave() Called")
+
+ newConfig = {}
+ for par_name, x1, x2, x3 in BACnet_parameters:
+ value = kwargs.get(par_name, None)
+ if value is not None:
+ newConfig[par_name] = value
+
+
+ # First check if configuration is OK.
+ if not _CheckWebConfiguration(newConfig):
+ return
+
+ # store to file the new configuration so that
+ # we can recoup the configuration the next time the PLC
+ # has a cold start (i.e. when Beremiz_service.py is retarted)
+ _SetSavedConfiguration(newConfig)
+
+ # Configure PLC with the current BACnet parameters
+ _SetPLCConfiguration(newConfig)
+
+
+
+def OnButtonReset(**kwargs):
+ """
+ # Function called when user clicks 'Delete' button in web interface
+ # The function will delete the file containing the persistent
+ # BACnet configution
+ """
+
+ # Delete the file
+ _DelSavedConfiguration()
+ # Set the current configuration to the default (hardcoded in C)
+ _SetPLCConfiguration(_DefaultConfiguration)
+ # Reset global variable
+ global _SavedConfiguration
+ _SavedConfiguration = None
+
+
+
+# location_str is replaced by extension's value in CTNGenerateC call
+def _runtime_bacnet_websettings_%(location_str)s_init():
+ """
+ # Callback function, called (by PLCObject.py) when a new PLC program
+ # (i.e. XXX.so file) is transfered to the PLC runtime
+ # and oaded into memory
+ """
+
+ #PLCObject.LogMessage("BACnet web server extension::OnLoadPLC() Called...")
+
+ if PLCObject.PLClibraryHandle is None:
+ # PLC was loaded but we don't have access to the library of compiled code (.so lib)?
+ # Hmm... This shold never occur!!
+ return
+
+ # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters
+ for name, web_label, c_dtype, web_dtype in BACnet_parameters:
+ # location_str is replaced by extension's value in CTNGenerateC call
+ GetParamFuncName = "__bacnet_%(location_str)s_get_ConfigParam_" + name
+ SetParamFuncName = "__bacnet_%(location_str)s_set_ConfigParam_" + name
+
+ # XXX TODO : stop reading from PLC .so file. This code is template code
+ # that can use modbus extension build data
+ GetParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, GetParamFuncName)
+ GetParamFuncs[name].restype = c_dtype
+ GetParamFuncs[name].argtypes = None
+
+ SetParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, SetParamFuncName)
+ SetParamFuncs[name].restype = None
+ SetParamFuncs[name].argtypes = [c_dtype]
+
+ # Default configuration is the configuration done in Beremiz IDE
+ # whose parameters get hardcoded into C, and compiled into the .so file
+ # We read the default configuration from the .so file before the values
+ # get changed by the user using the web server, or by the call (further on)
+ # to _SetPLCConfiguration(SavedConfiguration)
+ global _DefaultConfiguration
+ _DefaultConfiguration = _GetPLCConfiguration()
+
+ # Show the current PLC configuration on the web interface
+ global _WebviewConfiguration
+ _WebviewConfiguration = _GetPLCConfiguration()
+
+ # Read from file the last used configuration, which is likely
+ # different to the hardcoded configuration.
+ # We Reset the current configuration (i.e., the config stored in the
+ # variables of .so file) to this saved configuration
+ # so the PLC will start off with this saved configuration instead
+ # of the hardcoded (in Beremiz C generated code) configuration values.
+ #
+ # Note that _SetPLCConfiguration() will also update
+ # _WebviewConfiguration , if necessary.
+ global _SavedConfiguration
+ _SavedConfiguration = _GetSavedConfiguration()
+ if _SavedConfiguration is not None:
+ if _CheckConfiguration(_SavedConfiguration):
+ _SetPLCConfiguration(_SavedConfiguration)
+
+ WebSettings = NS.newExtensionSetting("BACnet extension", "bacnet_token")
+
+ # Configure the web interface to include the BACnet config parameters
+ WebSettings.addSettings(
+ "BACnetConfigParm", # name
+ _("BACnet Configuration"), # description
+ webFormInterface, # fields
+ _("Apply"), # button label
+ OnButtonSave) # callback
+
+ # Add the Delete button to the web interface
+ WebSettings.addSettings(
+ "BACnetConfigDelSaved", # name
+ _("BACnet Configuration"), # description
+ [ ("status",
+ annotate.String(label=_("Current state"),
+ immutable=True,
+ default=lambda *k:getConfigStatus())),
+ ], # fields (empty, no parameters required!)
+ _("Reset"), # button label
+ OnButtonReset)
+
+
+
+def getConfigStatus():
+ if _WebviewConfiguration == _DefaultConfiguration :
+ return "Unchanged"
+ return "Modified"
+
+
+# location_str is replaced by extension's value in CTNGenerateC call
+def _runtime_bacnet_websettings_%(location_str)s_cleanup():
+ """
+ # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
+ """
+
+ #PLCObject.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...")
+
+ NS.removeExtensionSetting("bacnet_token")
+
+ GetParamFuncs = {}
+ SetParamFuncs = {}
+ _WebviewConfiguration = None
+ _SavedConfiguration = None
+
+
+
+