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