bacnet/web_settings.py
branchsvghmi
changeset 2982 1627d552f181
parent 2676 b276d05bdb09
child 2686 703ebf57508a
equal deleted inserted replaced
2981:a0932a52e53b 2982:1627d552f181
       
     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 = os.path.join(WorkingDir, "bacnetconf.json")
       
    73 
       
    74 
       
    75 
       
    76 class BN_StrippedString(annotate.String):
       
    77     def __init__(self, *args, **kwargs):
       
    78         annotate.String.__init__(self, strip = True, *args, **kwargs)
       
    79 
       
    80 
       
    81 
       
    82 BACnet_parameters = [
       
    83     #    param. name             label                                            ctype type      annotate type
       
    84     # (C code var name)         (used on web interface)                          (C data type)    (web data type)
       
    85     #                                                                                             (annotate.String,
       
    86     #                                                                                              annotate.Integer, ...)
       
    87     ("network_interface"      , _("Network Interface")                         , ctypes.c_char_p, BN_StrippedString),
       
    88     ("port_number"            , _("UDP Port Number")                           , ctypes.c_char_p, BN_StrippedString),
       
    89     ("comm_control_passwd"    , _("BACnet Communication Control Password")     , ctypes.c_char_p, annotate.String),
       
    90     ("device_id"              , _("BACnet Device ID")                          , ctypes.c_int,    annotate.Integer),
       
    91     ("device_name"            , _("BACnet Device Name")                        , ctypes.c_char_p, annotate.String),
       
    92     ("device_location"        , _("BACnet Device Location")                    , ctypes.c_char_p, annotate.String),
       
    93     ("device_description"     , _("BACnet Device Description")                 , ctypes.c_char_p, annotate.String),
       
    94     ("device_appsoftware_ver" , _("BACnet Device Application Software Version"), ctypes.c_char_p, annotate.String)
       
    95     ]
       
    96 
       
    97 
       
    98 
       
    99 
       
   100 
       
   101 
       
   102 def _CheckPortnumber(port_number):
       
   103     """ check validity of the port number """
       
   104     try:
       
   105         portnum = int(port_number)
       
   106         if (portnum < 0) or (portnum > 65535):
       
   107            raise Exception
       
   108     except Exception:    
       
   109         return False
       
   110         
       
   111     return True    
       
   112     
       
   113 
       
   114 
       
   115 def _CheckDeviceID(device_id):
       
   116     """ 
       
   117     # check validity of the Device ID 
       
   118     # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID)
       
   119     #       so the Device instance ID is limited from 0 to 22^2-1 = 4194303
       
   120     #       However, 4194303 is reserved for special use (similar to NULL pointer), so last
       
   121     #       valid ID becomes 4194302
       
   122     """
       
   123     try:
       
   124         devid = int(device_id)
       
   125         if (devid < 0) or (devid > 4194302):
       
   126             raise Exception
       
   127     except Exception:    
       
   128         return False
       
   129         
       
   130     return True    
       
   131 
       
   132 
       
   133 
       
   134 
       
   135 
       
   136 def _CheckConfiguration(BACnetConfig):
       
   137     res = True    
       
   138     res = res and _CheckPortnumber(BACnetConfig["port_number"])
       
   139     res = res and _CheckDeviceID  (BACnetConfig["device_id"])
       
   140     return res
       
   141 
       
   142 
       
   143 
       
   144 def _CheckWebConfiguration(BACnetConfig):
       
   145     res = True
       
   146     
       
   147     # check the port number
       
   148     if not _CheckPortnumber(BACnetConfig["port_number"]):
       
   149         raise annotate.ValidateError(
       
   150             {"port_number": "Invalid port number: " + str(BACnetConfig["port_number"])},
       
   151             _("BACnet configuration error:"))
       
   152         res = False
       
   153     
       
   154     if not _CheckDeviceID(BACnetConfig["device_id"]):
       
   155         raise annotate.ValidateError(
       
   156             {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])},
       
   157             _("BACnet configuration error:"))
       
   158         res = False
       
   159         
       
   160     return res
       
   161 
       
   162 
       
   163 
       
   164 
       
   165 
       
   166 
       
   167 def _SetSavedConfiguration(BACnetConfig):
       
   168     """ Stores in a file a dictionary containing the BACnet parameter configuration """
       
   169     global _SavedConfiguration
       
   170 
       
   171     if BACnetConfig == _DefaultConfiguration :
       
   172         _DelSavedConfiguration()
       
   173         _SavedConfiguration = None
       
   174     else :
       
   175         with open(os.path.realpath(_BACnetConfFilename), 'w') as f:
       
   176             json.dump(BACnetConfig, f, sort_keys=True, indent=4)
       
   177         _SavedConfiguration = BACnetConfig
       
   178 
       
   179 
       
   180 def _DelSavedConfiguration():
       
   181     """ Deletes the file cotaining the persistent BACnet configuration """
       
   182     if os.path.exists(_BACnetConfFilename):
       
   183         os.remove(_BACnetConfFilename)
       
   184 
       
   185 
       
   186 def _GetSavedConfiguration():
       
   187     """
       
   188     # Returns a dictionary containing the BACnet parameter configuration
       
   189     # that was last saved to file. If no file exists, then return None
       
   190     """
       
   191     try:
       
   192         #if os.path.isfile(_BACnetConfFilename):
       
   193         saved_config = json.load(open(_BACnetConfFilename))
       
   194     except Exception:    
       
   195         return None
       
   196 
       
   197     if _CheckConfiguration(saved_config):
       
   198         return saved_config
       
   199     else:
       
   200         return None
       
   201 
       
   202 
       
   203 def _GetPLCConfiguration():
       
   204     """
       
   205     # Returns a dictionary containing the current BACnet parameter configuration
       
   206     # stored in the C variables in the loaded PLC (.so file)
       
   207     """
       
   208     current_config = {}
       
   209     for par_name, x1, x2, x3 in BACnet_parameters:
       
   210         value = GetParamFuncs[par_name]()
       
   211         if value is not None:
       
   212             current_config[par_name] = value
       
   213     
       
   214     return current_config
       
   215 
       
   216 
       
   217 def _SetPLCConfiguration(BACnetConfig):
       
   218     """
       
   219     # Stores the BACnet parameter configuration into the
       
   220     # the C variables in the loaded PLC (.so file)
       
   221     """
       
   222     for par_name in BACnetConfig:
       
   223         value = BACnetConfig[par_name]
       
   224         #PLCObject.LogMessage("BACnet web server extension::_SetPLCConfiguration()  Setting "
       
   225         #                       + par_name + " to " + str(value) )
       
   226         if value is not None:
       
   227             SetParamFuncs[par_name](value)
       
   228     # update the configuration shown on the web interface
       
   229     global _WebviewConfiguration 
       
   230     _WebviewConfiguration = _GetPLCConfiguration()
       
   231 
       
   232 
       
   233 
       
   234 def _GetWebviewConfigurationValue(ctx, argument):
       
   235     """
       
   236     # Callback function, called by the web interface (NevowServer.py)
       
   237     # to fill in the default value of each parameter
       
   238     """
       
   239     try:
       
   240         return _WebviewConfiguration[argument.name]
       
   241     except Exception:
       
   242         return ""
       
   243 
       
   244 
       
   245 # The configuration of the web form used to see/edit the BACnet parameters
       
   246 webFormInterface = [(name, web_dtype (label=web_label, default=_GetWebviewConfigurationValue)) 
       
   247                     for name, web_label, c_dtype, web_dtype in BACnet_parameters]
       
   248 
       
   249 
       
   250 def OnButtonSave(**kwargs):
       
   251     """
       
   252     # Function called when user clicks 'Save' button in web interface
       
   253     # The function will configure the BACnet plugin in the PLC with the values
       
   254     # specified in the web interface. However, values must be validated first!
       
   255     """
       
   256 
       
   257     #PLCObject.LogMessage("BACnet web server extension::OnButtonSave()  Called")
       
   258     
       
   259     newConfig = {}
       
   260     for par_name, x1, x2, x3 in BACnet_parameters:
       
   261         value = kwargs.get(par_name, None)
       
   262         if value is not None:
       
   263             newConfig[par_name] = value
       
   264 
       
   265     
       
   266     # First check if configuration is OK.
       
   267     if not _CheckWebConfiguration(newConfig):
       
   268         return
       
   269 
       
   270     # store to file the new configuration so that 
       
   271     # we can recoup the configuration the next time the PLC
       
   272     # has a cold start (i.e. when Beremiz_service.py is retarted)
       
   273     _SetSavedConfiguration(newConfig)
       
   274 
       
   275     # Configure PLC with the current BACnet parameters
       
   276     _SetPLCConfiguration(newConfig)
       
   277 
       
   278 
       
   279 
       
   280 def OnButtonReset(**kwargs):
       
   281     """
       
   282     # Function called when user clicks 'Delete' button in web interface
       
   283     # The function will delete the file containing the persistent
       
   284     # BACnet configution
       
   285     """
       
   286 
       
   287     # Delete the file
       
   288     _DelSavedConfiguration()
       
   289     # Set the current configuration to the default (hardcoded in C)
       
   290     _SetPLCConfiguration(_DefaultConfiguration)
       
   291     # Reset global variable
       
   292     global _SavedConfiguration
       
   293     _SavedConfiguration = None
       
   294 
       
   295 
       
   296 
       
   297 # location_str is replaced by extension's value in CTNGenerateC call
       
   298 def _runtime_bacnet_websettings_%(location_str)s_init():
       
   299     """
       
   300     # Callback function, called (by PLCObject.py) when a new PLC program
       
   301     # (i.e. XXX.so file) is transfered to the PLC runtime
       
   302     # and oaded into memory
       
   303     """
       
   304 
       
   305     #PLCObject.LogMessage("BACnet web server extension::OnLoadPLC() Called...")
       
   306 
       
   307     if PLCObject.PLClibraryHandle is None:
       
   308         # PLC was loaded but we don't have access to the library of compiled code (.so lib)?
       
   309         # Hmm... This shold never occur!! 
       
   310         return  
       
   311 
       
   312     # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters
       
   313     for name, web_label, c_dtype, web_dtype in BACnet_parameters:
       
   314         # location_str is replaced by extension's value in CTNGenerateC call
       
   315         GetParamFuncName = "__bacnet_%(location_str)s_get_ConfigParam_" + name
       
   316         SetParamFuncName = "__bacnet_%(location_str)s_set_ConfigParam_" + name
       
   317         
       
   318         # XXX TODO : stop reading from PLC .so file. This code is template code
       
   319         #            that can use modbus extension build data
       
   320         GetParamFuncs[name]          = getattr(PLCObject.PLClibraryHandle, GetParamFuncName)
       
   321         GetParamFuncs[name].restype  = c_dtype
       
   322         GetParamFuncs[name].argtypes = None
       
   323         
       
   324         SetParamFuncs[name]          = getattr(PLCObject.PLClibraryHandle, SetParamFuncName)
       
   325         SetParamFuncs[name].restype  = None
       
   326         SetParamFuncs[name].argtypes = [c_dtype]
       
   327 
       
   328     # Default configuration is the configuration done in Beremiz IDE
       
   329     # whose parameters get hardcoded into C, and compiled into the .so file
       
   330     # We read the default configuration from the .so file before the values
       
   331     # get changed by the user using the web server, or by the call (further on)
       
   332     # to _SetPLCConfiguration(SavedConfiguration)
       
   333     global _DefaultConfiguration 
       
   334     _DefaultConfiguration = _GetPLCConfiguration()
       
   335     
       
   336     # Show the current PLC configuration on the web interface        
       
   337     global _WebviewConfiguration
       
   338     _WebviewConfiguration = _GetPLCConfiguration()
       
   339  
       
   340     # Read from file the last used configuration, which is likely
       
   341     # different to the hardcoded configuration.
       
   342     # We Reset the current configuration (i.e., the config stored in the 
       
   343     # variables of .so file) to this saved configuration
       
   344     # so the PLC will start off with this saved configuration instead
       
   345     # of the hardcoded (in Beremiz C generated code) configuration values.
       
   346     #
       
   347     # Note that _SetPLCConfiguration() will also update 
       
   348     # _WebviewConfiguration , if necessary.
       
   349     global _SavedConfiguration
       
   350     _SavedConfiguration  = _GetSavedConfiguration()
       
   351     if _SavedConfiguration is not None:
       
   352         if _CheckConfiguration(_SavedConfiguration):
       
   353             _SetPLCConfiguration(_SavedConfiguration)
       
   354             
       
   355     WebSettings = NS.newExtensionSetting("BACnet extension", "bacnet_token")
       
   356 
       
   357     # Configure the web interface to include the BACnet config parameters
       
   358     WebSettings.addSettings(
       
   359         "BACnetConfigParm",                # name
       
   360         _("BACnet Configuration"),         # description
       
   361         webFormInterface,                  # fields
       
   362         _("Apply"),  # button label
       
   363         OnButtonSave)                      # callback    
       
   364 
       
   365     # Add the Delete button to the web interface
       
   366     WebSettings.addSettings(
       
   367         "BACnetConfigDelSaved",                   # name
       
   368         _("BACnet Configuration"),                # description
       
   369         [ ("status",
       
   370            annotate.String(label=_("Current state"),
       
   371                            immutable=True,
       
   372                            default=lambda *k:getConfigStatus())),
       
   373         ],                                       # fields  (empty, no parameters required!)
       
   374         _("Reset"), # button label
       
   375         OnButtonReset) 
       
   376 
       
   377 
       
   378 
       
   379 def getConfigStatus():
       
   380     if _WebviewConfiguration == _DefaultConfiguration :
       
   381         return "Unchanged"
       
   382     return "Modified"
       
   383 
       
   384 
       
   385 # location_str is replaced by extension's value in CTNGenerateC call
       
   386 def _runtime_bacnet_websettings_%(location_str)s_cleanup():
       
   387     """
       
   388     # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
       
   389     """
       
   390 
       
   391     #PLCObject.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...")
       
   392     
       
   393     NS.removeExtensionSetting("bacnet_token")
       
   394     
       
   395     GetParamFuncs = {}
       
   396     SetParamFuncs = {}
       
   397     _WebviewConfiguration = None
       
   398     _SavedConfiguration   = None
       
   399 
       
   400 
       
   401 
       
   402