#!/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