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