# HG changeset patch # User Tomaz Orac # Date 1592315632 -7200 # Node ID 45f1ff6a7f877b018df7895e48b7c3b86b12de42 # Parent 556935640ec068c53457441dafadf778a666b6ad# Parent 8b612b357679cd074b099c9ab2c59fc99b884206 Merge diff -r 8b612b357679 -r 45f1ff6a7f87 Beremiz_service.py --- a/Beremiz_service.py Tue Jun 02 13:37:34 2020 +0200 +++ b/Beremiz_service.py Tue Jun 16 15:53:52 2020 +0200 @@ -497,10 +497,10 @@ if webport is not None: try: import runtime.NevowServer as NS # pylint: disable=ungrouped-imports + NS.WorkingDir = WorkingDir except Exception: LogMessageAndException(_("Nevow/Athena import failed :")) webport = None - NS.WorkingDir = WorkingDir try: import runtime.WampClient as WC # pylint: disable=ungrouped-imports diff -r 8b612b357679 -r 45f1ff6a7f87 bacnet/bacnet.py --- a/bacnet/bacnet.py Tue Jun 02 13:37:34 2020 +0200 +++ b/bacnet/bacnet.py Tue Jun 16 15:53:52 2020 +0200 @@ -35,11 +35,12 @@ from bacnet.BacnetSlaveEditor import * from bacnet.BacnetSlaveEditor import ObjectProperties from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY - -base_folder = os.path.split( - os.path.dirname(os.path.realpath(__file__)))[0] +from ConfigTreeNode import ConfigTreeNode +import util.paths as paths + +base_folder = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] base_folder = os.path.join(base_folder, "..") -BacnetPath = os.path.join(base_folder, "BACnet") +BacnetPath = os.path.join(base_folder, "BACnet") BacnetLibraryPath = os.path.join(BacnetPath, "lib") BacnetIncludePath = os.path.join(BacnetPath, "include") BacnetIncludePortPath = os.path.join(BacnetPath, "ports") @@ -50,6 +51,10 @@ BACNET_VENDOR_NAME = "Beremiz.org" BACNET_DEVICE_MODEL_NAME = "Beremiz PLC" + +# Max String Size of BACnet Paramaters +BACNET_PARAM_STRING_SIZE = 64 + # # # @@ -97,6 +102,14 @@ """ + # NOTE; Add the following code/declaration to the aboce XSD in order to activate the + # Override_Parameters_Saved_on_PLC flag (currenty not in use as it requires further + # analysis how the user would interpret this user interface option. + # <--- snip ---> + # + # <--- snip ---> + # # 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 @@ -552,8 +565,10 @@ generate_file_handle .write(generate_file_content) generate_file_handle .close() - # - # Generate the source files # + + + # + # Generate the C source code files # def CTNGenerate_C(self, buildpath, locations): # Determine the current location in Beremiz's project configuration @@ -596,6 +611,11 @@ # The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers() # It will be an XML parser object created by # GenerateParserFromXSDstring(self.XSD).CreateRoot() + # + # Note: Override_Parameters_Saved_on_PLC is converted to an integer by int() + # The above flag is not currently in use. It requires further thinking on how the + # user will interpret and interact with this user interface... + #loc_dict["Override_Parameters_Saved_on_PLC"] = int(self.BACnetServerNode.getOverride_Parameters_Saved_on_PLC()) loc_dict["network_interface"] = self.BACnetServerNode.getNetwork_Interface() loc_dict["port_number"] = self.BACnetServerNode.getUDP_Port_Number() loc_dict["BACnet_Device_ID"] = self.BACnetServerNode.getBACnet_Device_ID() @@ -607,6 +627,8 @@ loc_dict["BACnet_Vendor_ID"] = BACNET_VENDOR_ID loc_dict["BACnet_Vendor_Name"] = BACNET_VENDOR_NAME loc_dict["BACnet_Model_Name"] = BACNET_DEVICE_MODEL_NAME + loc_dict["BACnet_Param_String_Size"] = BACNET_PARAM_STRING_SIZE + # 2) Add the data specific to each BACnet object type # For each BACnet object type, start off by creating some intermediate helpful lists @@ -726,4 +748,48 @@ CFLAGS = ' -I"' + BacnetIncludePath + '"' CFLAGS += ' -I"' + BacnetIncludePortPath + '"' - return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True + # ---------------------------------------------------------------------- + # Create a file containing the default configuration paramters. + # Beremiz will then transfer this file to the PLC, where the web server + # will read it to obtain the default configuration parameters. + # ---------------------------------------------------------------------- + # NOTE: This is no loner needed! The web interface will read these + # parameters directly from the compiled C code (.so file) + # + ### extra_file_name = os.path.join(buildpath, "%s_%s.%s" % ('bacnet_extrafile', postfix, 'txt')) + ### extra_file_handle = open(extra_file_name, 'w') + ### + ### proplist = ["network_interface", "port_number", "BACnet_Device_ID", "BACnet_Device_Name", + ### "BACnet_Comm_Control_Password", "BACnet_Device_Location", + ### "BACnet_Device_Description", "BACnet_Device_AppSoft_Version"] + ### for propname in proplist: + ### extra_file_handle.write("%s:%s\n" % (propname, loc_dict[propname])) + ### + ### extra_file_handle.close() + ### extra_file_handle = open(extra_file_name, 'r') + + # Format of data to return: + # [(Cfiles, CFLAGS), ...], LDFLAGS, DoCalls, extra_files + # LDFLAGS = ['flag1', 'flag2', ...] + # DoCalls = true or false + # extra_files = (fname,fobject), ... + # fobject = file object, already open'ed for read() !! + # + # extra_files -> files that will be downloaded to the PLC! + + websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r') + websettingcode = websettingfile.read() + websettingfile.close() + + location_str = "_".join(map(str, self.GetCurrentLocation())) + websettingcode = websettingcode % locals() + + runtimefile_path = os.path.join(buildpath, "runtime_bacnet_websettings.py") + runtimefile = open(runtimefile_path, 'w') + runtimefile.write(websettingcode) + runtimefile.close() + + return ([(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, + ("runtime_bacnet_websettings_%s.py" % location_str, open(runtimefile_path, "rb")), + ) + #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle) diff -r 8b612b357679 -r 45f1ff6a7f87 bacnet/runtime/device.h --- a/bacnet/runtime/device.h Tue Jun 02 13:37:34 2020 +0200 +++ b/bacnet/runtime/device.h Tue Jun 16 15:53:52 2020 +0200 @@ -161,11 +161,12 @@ * Maximum sizes excluding nul terminator . */ #define STRLEN_X(minlen, str) (((minlen)>sizeof(str))?(minlen):sizeof(str)) -#define MAX_DEV_NAME_LEN STRLEN_X(32, "%(BACnet_Device_Name)s") /* Device name */ -#define MAX_DEV_LOC_LEN STRLEN_X(64, BACNET_DEVICE_LOCATION) /* Device location */ -#define MAX_DEV_MOD_LEN STRLEN_X(32, BACNET_DEVICE_MODEL_NAME) /* Device model name */ -#define MAX_DEV_VER_LEN STRLEN_X(16, BACNET_DEVICE_APPSOFT_VER) /* Device application software version */ -#define MAX_DEV_DESC_LEN STRLEN_X(64, BACNET_DEVICE_DESCRIPTION) /* Device description */ +#define MAX_DEV_NAME_LEN STRLEN_X(%(BACnet_Param_String_Size)d, "%(BACnet_Device_Name)s") /* Device name */ +#define MAX_DEV_LOC_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_LOCATION) /* Device location */ +#define MAX_DEV_MOD_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_MODEL_NAME) /* Device model name */ +#define MAX_DEV_VER_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_APPSOFT_VER) /* Device application software version */ +#define MAX_DEV_DESC_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_DESCRIPTION) /* Device description */ + /** Structure to define the Object Properties common to all Objects. */ typedef struct commonBacObj_s { diff -r 8b612b357679 -r 45f1ff6a7f87 bacnet/runtime/server.c --- a/bacnet/runtime/server.c Tue Jun 02 13:37:34 2020 +0200 +++ b/bacnet/runtime/server.c Tue Jun 16 15:53:52 2020 +0200 @@ -52,6 +52,10 @@ #include "timesync.h" + + + + /* A utility function used by most (all?) implementations of BACnet Objects */ /* Adds to Prop_List all entries in Prop_List_XX that are not * PROP_OBJECT_IDENTIFIER, PROP_OBJECT_NAME, PROP_OBJECT_TYPE, PROP_PROPERTY_LIST @@ -454,17 +458,33 @@ bvlc_bdt_restore_local(); /* Initiliaze the bacnet server 'device' */ Device_Init(server_node->device_name); - - pthread_mutex_lock(&init_done_lock); - init_done = 1; - pthread_cond_signal(&init_done_cond); - pthread_mutex_unlock(&init_done_lock); - + + /* Although the default values for the following properties are hardcoded into + * the respective variable definition+initialization in the C code, + * these values may be potentially changed after compilation but before + * code startup. This is done by the web interface(1), directly loading the .so file, + * and changing the values in the server_node_t variable. + * We must therefore honor those values when we start running the BACnet device server + * + * (1) Web interface is implemented in runtime/BACnet_config.py + * which works as an extension of the web server in runtime/NevowServer.py + * which in turn is initialised/run by the Beremiz_service.py deamon + */ + Device_Set_Location (server_node->device_location, strlen(server_node->device_location)); + Device_Set_Description (server_node->device_description, strlen(server_node->device_description)); + Device_Set_Application_Software_Version(server_node->device_appsoftware_ver, strlen(server_node->device_appsoftware_ver)); /* Set the password (max 31 chars) for Device Communication Control request. */ /* Default in the BACnet stack is hardcoded as "filister" */ /* (char *) cast is to remove the cast. The function is incorrectly declared/defined in the BACnet stack! */ /* BACnet stack needs to change demo/handler/h_dcc.c and include/handlers.h */ handler_dcc_password_set((char *)server_node->comm_control_passwd); + + + pthread_mutex_lock(&init_done_lock); + init_done = 1; + pthread_cond_signal(&init_done_cond); + pthread_mutex_unlock(&init_done_lock); + /* Set callbacks and configure network interface */ res = Init_Service_Handlers(); if (res < 0) exit(1); @@ -661,3 +681,108 @@ return res; } + + + + +/**********************************************/ +/** Functions for Beremiz web interface. **/ +/**********************************************/ + +/* + * Beremiz has a program to run on the PLC (Beremiz_service.py) + * to handle downloading of compiled programs, start/stop of PLC, etc. + * (see runtime/PLCObject.py for start/stop, loading, ...) + * + * This service also includes a web server to access PLC state (start/stop) + * and to change some basic confiuration parameters. + * (see runtime/NevowServer.py for the web server) + * + * The web server allows for extensions, where additional configuration + * parameters may be changed on the running/downloaded PLC. + * BACnet plugin also comes with an extension to the web server, through + * which the basic BACnet plugin configuration parameters may be changed + * (basically, only the parameters in server_node_t may be changed) + * + * The following functions are never called from other C code. They are + * called instead from the python code in runtime/BACnet_config.py, that + * implements the web server extension for configuring BACnet parameters. + */ + + +/* The location (in the Config. Node Tree of Beremiz IDE) + * of the BACnet plugin. + * + * This variable is also used by the BACnet web config code to determine + * whether the current loaded PLC includes the BACnet plugin + * (so it should make the BACnet parameter web interface visible to the user). + */ +const char * __bacnet_plugin_location = "%(locstr)s"; + + +/* NOTE: We could have the python code in runtime/BACnet_config.py + * directly access the server_node_t structure, however + * this would create a tight coupling between these two + * disjoint pieces of code. + * Any change to the server_node_t structure would require the + * python code to be changed accordingly. I have therefore opted + * to cretae get/set functions, one for each parameter. + * + * NOTE: since the BACnet plugin can only support/allow at most one instance + * of the BACnet plugin in Beremiz (2 or more are not allowed due to + * limitations of the underlying BACnet protocol stack being used), + * a single generic version of each of the following functions would work. + * However, simply for the sake of keeping things general, we create + * a diferent function for each plugin instance (which, as I said, + * will never occur for now). + * + * The functions being declared are therefoe named: + * __bacnet_0_get_ConfigParam_location + * __bacnet_0_get_ConfigParam_device_name + * etc... + * where the 0 will be replaced by the location of the BACnet plugin + * in the Beremiz configuration tree (will change depending on where + * the user inserted the BACnet plugin into their project) + */ + +/* macro works for all data types */ +#define __bacnet_get_ConfigParam(param_type,param_name) \ +param_type __bacnet_%(locstr)s_get_ConfigParam_##param_name(void) { \ + return server_node.param_name; \ +} + +/* macro only works for char * data types */ +/* Note that the storage space (max string size) reserved for each parameter + * (this storage space is reserved in device.h) + * is set to a minimum of + * %(BACnet_Param_String_Size)d + * which is set to the value of + * BACNET_PARAM_STRING_SIZE + * in bacnet.py + */ +#define __bacnet_set_ConfigParam(param_type,param_name) \ +void __bacnet_%(locstr)s_set_ConfigParam_##param_name(param_type val) { \ + strncpy(server_node.param_name, val, %(BACnet_Param_String_Size)d); \ + server_node.param_name[%(BACnet_Param_String_Size)d - 1] = '\0'; \ +} + + +#define __bacnet_ConfigParam_str(param_name) \ +__bacnet_get_ConfigParam(const char*,param_name) \ +__bacnet_set_ConfigParam(const char*,param_name) + + +__bacnet_ConfigParam_str(location) +__bacnet_ConfigParam_str(network_interface) +__bacnet_ConfigParam_str(port_number) +__bacnet_ConfigParam_str(device_name) +__bacnet_ConfigParam_str(device_location) +__bacnet_ConfigParam_str(device_description) +__bacnet_ConfigParam_str(device_appsoftware_ver) +__bacnet_ConfigParam_str(comm_control_passwd) + +__bacnet_get_ConfigParam(uint32_t,device_id) +void __bacnet_%(locstr)s_set_ConfigParam_device_id(uint32_t val) { + server_node.device_id = val; +} + diff -r 8b612b357679 -r 45f1ff6a7f87 bacnet/runtime/server.h --- a/bacnet/runtime/server.h Tue Jun 02 13:37:34 2020 +0200 +++ b/bacnet/runtime/server.h Tue Jun 16 15:53:52 2020 +0200 @@ -33,12 +33,19 @@ + typedef struct{ - const char *location; - const char *network_interface; - const char *port_number; - const char *device_name; - const char *comm_control_passwd; + char location [%(BACnet_Param_String_Size)d]; + char network_interface [%(BACnet_Param_String_Size)d]; + char port_number [%(BACnet_Param_String_Size)d]; + char device_name [%(BACnet_Param_String_Size)d]; + char device_location [%(BACnet_Param_String_Size)d]; + char device_description [%(BACnet_Param_String_Size)d]; + char device_appsoftware_ver[%(BACnet_Param_String_Size)d]; + char comm_control_passwd [%(BACnet_Param_String_Size)d]; +// int override_local_config; // bool flag => +// // true : use these parameter values +// // false: use values stored on local file in PLC uint32_t device_id; // device ID is 22 bits long! uint16_t is not enough! int init_state; // store how far along the server's initialization has progressed pthread_t thread_id; // thread handling this server @@ -49,13 +56,16 @@ /*initialization following all parameters given by user in application*/ static server_node_t server_node = { "%(locstr)s", - "%(network_interface)s", // interface (NULL => use default (eth0)) - "%(port_number)s", // Port number (NULL => use default) - "%(BACnet_Device_Name)s", // BACnet server's device (object) name - "%(BACnet_Comm_Control_Password)s",// BACnet server's device (object) name - %(BACnet_Device_ID)s // BACnet server's device (object) ID + "%(network_interface)s", // interface (NULL => use default (eth0)) + "%(port_number)s", // Port number (NULL => use default) + "%(BACnet_Device_Name)s", // BACnet server's device (object) Name + "%(BACnet_Device_Location)s", // BACnet server's device (object) Location + "%(BACnet_Device_Description)s", // BACnet server's device (object) Description + "%(BACnet_Device_AppSoft_Version)s", // BACnet server's device (object) App. Software Ver. + "%(BACnet_Comm_Control_Password)s", // BACnet server's device (object) Password +// (Override_Parameters_Saved_on_PLC)d, // override locally saved parameters (bool flag) + %(BACnet_Device_ID)s // BACnet server's device (object) ID }; - #endif /* SERVER_H_ */ diff -r 8b612b357679 -r 45f1ff6a7f87 bacnet/web_settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bacnet/web_settings.py Tue Jun 16 15:53:52 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 + + + + diff -r 8b612b357679 -r 45f1ff6a7f87 modbus/mb_runtime.c --- a/modbus/mb_runtime.c Tue Jun 02 13:37:34 2020 +0200 +++ b/modbus/mb_runtime.c Tue Jun 16 15:53:52 2020 +0200 @@ -25,6 +25,8 @@ #include #include /* required for memcpy() */ +#include +#include #include "mb_slave_and_master.h" #include "MB_%(locstr)s.h" @@ -299,10 +301,42 @@ // Enable thread cancelation. Enabled is default, but set it anyway to be safe. pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - // get the current time - clock_gettime(CLOCK_MONOTONIC, &next_cycle); - - // loop the communication with the client + // configure the timer for periodic activation + { + struct itimerspec timerspec; + timerspec.it_interval.tv_sec = period_sec; + timerspec.it_interval.tv_nsec = period_nsec; + timerspec.it_value = timerspec.it_interval; + + if (timer_settime(client_nodes[client_node_id].timer_id, 0 /* flags */, &timerspec, NULL) < 0) + fprintf(stderr, "Modbus plugin: Error configuring periodic activation timer for Modbus client %%s.\n", client_nodes[client_node_id].location); + } + + /* loop the communication with the client + * + * When the client thread has difficulty communicating with remote client and/or server (network issues, for example), + * then the communications get delayed and we will fall behind in the period. + * + * This is OK. Note that if the condition variable were to be signaled multiple times while the client thread is inside the same + * Modbus transaction, then all those signals would be ignored. + * However, and since we keep the mutex locked during the communication cycle, it is not possible to signal the condition variable + * during that time (it is only possible while the thread is blocked during the call to pthread_cond_wait(). + * + * This means that when network issues eventually get resolved, we will NOT have a bunch of delayed activations to handle + * in quick succession (which would goble up CPU time). + * + * Notice that the above property is valid whether the communication cycle is run with the mutex locked, or unlocked. + * Since it makes it easier to implement the correct semantics for the other activation methods if the communication cycle + * is run with the mutex locked, then that is what we do. + * + * Note that during all the communication cycle we will keep locked the mutex + * (i.e. the mutex used together with the condition variable that will activate a new communication cycle) + * + * Note that we never get to explicitly unlock this mutex. It will only be unlocked by the pthread_cond_wait() + * call at the end of the cycle. + */ + pthread_mutex_lock(&(client_nodes[client_node_id].mutex)); + while (1) { /* struct timespec cur_time; @@ -311,9 +345,22 @@ */ int req; for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){ - /*just do the requests belonging to the client */ + /* just do the requests belonging to the client */ if (client_requests[req].client_node_id != client_node_id) continue; + + /* only do the request if: + * - this request was explictly asked to be executed by the client program + * OR + * - the client thread was activated periodically + * (in which case we execute all the requests belonging to the client node) + */ + if ((client_requests[req].flag_exec_req == 0) && (client_nodes[client_requests[req].client_node_id].periodic_act == 0)) + continue; + + //fprintf(stderr, "Modbus plugin: RUNNING<###> of Modbus request %%d (periodic = %%d flag_exec_req = %%d)\n", + // req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req ); + int res_tmp = __execute_mb_request(req); switch (res_tmp) { case PORT_FAILURE: { @@ -357,36 +404,40 @@ break; } } - } - // Determine absolute time instant for starting the next cycle - struct timespec prev_cycle, now; - prev_cycle = next_cycle; - timespec_add(next_cycle, period_sec, period_nsec); - /* NOTE A: - * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will - * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously - * for some time until we catch up. - * This is undesirable, so we detect it by making sure the next_cycle will start in the future. - * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval. - * - * NOTE B: - * It probably does not make sense to check for overflow of timer - so we don't do it for now! - * Even in 32 bit systems this will take at least 68 years since the computer booted - * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0 - * every time the system boots). On 64 bit systems, it will take over - * 10^11 years to overflow. - */ - clock_gettime(CLOCK_MONOTONIC, &now); - if ( ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec))) - /* We are falling behind. See NOTE A above */ - || (next_cycle.tv_sec < prev_cycle.tv_sec) - /* Timer overflow. See NOTE B above */ - ) { - next_cycle = now; - timespec_add(next_cycle, period_sec, period_nsec); - } - - clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL); + + /* We have just finished excuting a client transcation request. + * If the current cycle was activated by user request we reset the flag used to ask to run it + */ + if (0 != client_requests[req].flag_exec_req) { + client_requests[req].flag_exec_req = 0; + client_requests[req].flag_exec_started = 0; + } + + //fprintf(stderr, "Modbus plugin: RUNNING<---> of Modbus request %%d (periodic = %%d flag_exec_req = %%d)\n", + // req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req ); + } + + // Wait for signal (from timer or explicit request from user program) before starting the next cycle + { + // No need to lock the mutex. Is is already locked just before the while(1) loop. + // Read the comment there to understand why. + // pthread_mutex_lock(&(client_nodes[client_node_id].mutex)); + + /* the client thread has just finished a cycle, so all the flags used to signal an activation + * and specify the activation source (periodic, user request, ...) + * get reset here, before waiting for a new activation. + */ + client_nodes[client_node_id].periodic_act = 0; + client_nodes[client_node_id].execute_req = 0; + + while (client_nodes[client_node_id].execute_req == 0) + pthread_cond_wait(&(client_nodes[client_node_id].condv), + &(client_nodes[client_node_id].mutex)); + + // We run the communication cycle with the mutex locked. + // Read the comment just above the while(1) to understand why. + // pthread_mutex_unlock(&(client_nodes[client_node_id].mutex)); + } } // humour the compiler. @@ -394,18 +445,85 @@ } + +/* Function to activate a client node's thread */ +/* returns -1 if it could not send the signal */ +static int __signal_client_thread(int client_node_id) { + /* We TRY to signal the client thread. + * We do this because this function can be called at the end of the PLC scan cycle + * and we don't want it to block at that time. + */ + if (pthread_mutex_trylock(&(client_nodes[client_node_id].mutex)) != 0) + return -1; + client_nodes[client_node_id].execute_req = 1; // tell the thread to execute + pthread_cond_signal (&(client_nodes[client_node_id].condv)); + pthread_mutex_unlock(&(client_nodes[client_node_id].mutex)); + return 0; +} + + + +/* Function that will be called whenever a client node's periodic timer expires. */ +/* The client node's thread will be waiting on a condition variable, so this function simply signals that + * condition variable. + * + * The same callback function is called by the timers of all client nodes. The id of the client node + * in question will be passed as a parameter to the call back function. + */ +void __client_node_timer_callback_function(union sigval sigev_value) { + /* signal the client node's condition variable on which the client node's thread should be waiting... */ + /* Since the communication cycle is run with the mutex locked, we use trylock() instead of lock() */ + //pthread_mutex_lock (&(client_nodes[sigev_value.sival_int].mutex)); + if (pthread_mutex_trylock (&(client_nodes[sigev_value.sival_int].mutex)) != 0) + /* we never get to signal the thread for activation. But that is OK. + * If it still in the communication cycle (during which the mutex is kept locked) + * then that means that the communication cycle is falling behing in the periodic + * communication cycle, and we therefore need to skip a period. + */ + return; + client_nodes[sigev_value.sival_int].execute_req = 1; // tell the thread to execute + client_nodes[sigev_value.sival_int].periodic_act = 1; // tell the thread the activation was done by periodic timer + pthread_cond_signal (&(client_nodes[sigev_value.sival_int].condv)); + pthread_mutex_unlock(&(client_nodes[sigev_value.sival_int].mutex)); +} + + + int __cleanup_%(locstr)s (); int __init_%(locstr)s (int argc, char **argv){ int index; - for (index=0; index < NUMBER_OF_CLIENT_NODES;index++) + for (index=0; index < NUMBER_OF_CLIENT_NODES;index++) { client_nodes[index].mb_nd = -1; - for (index=0; index < NUMBER_OF_SERVER_NODES;index++) + /* see comment in mb_runtime.h to understad why we need to initialize these entries */ + switch (client_nodes[index].node_address.naf) { + case naf_tcp: + client_nodes[index].node_address.addr.tcp.host = client_nodes[index].str1; + client_nodes[index].node_address.addr.tcp.service = client_nodes[index].str2; + break; + case naf_rtu: + client_nodes[index].node_address.addr.rtu.device = client_nodes[index].str1; + break; + } + } + + for (index=0; index < NUMBER_OF_SERVER_NODES;index++) { // mb_nd with negative numbers indicate how far it has been initialised (or not) // -2 --> no modbus node created; no thread created // -1 --> modbus node created!; no thread created // >=0 --> modbus node created!; thread created! server_nodes[index].mb_nd = -2; + /* see comment in mb_runtime.h to understad why we need to initialize these entries */ + switch (server_nodes[index].node_address.naf) { + case naf_tcp: + server_nodes[index].node_address.addr.tcp.host = server_nodes[index].str1; + server_nodes[index].node_address.addr.tcp.service = server_nodes[index].str2; + break; + case naf_rtu: + server_nodes[index].node_address.addr.rtu.device = server_nodes[index].str1; + break; + } + } /* modbus library init */ /* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus @@ -421,9 +539,14 @@ return -1; } - /* init the mutex for each client request */ + /* init each client request */ /* Must be done _before_ launching the client threads!! */ for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ + /* make sure flags connected to user program MB transaction start request are all reset */ + client_requests[index].flag_exec_req = 0; + client_requests[index].flag_exec_started = 0; + /* init the mutex for each client request */ + /* Must be done _before_ launching the client threads!! */ if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) { fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location); goto error_exit; @@ -443,6 +566,39 @@ } client_nodes[index].init_state = 1; // we have created the node + /* initialize the mutex variable that will be used by the thread handling the client node */ + if (pthread_mutex_init(&(client_nodes[index].mutex), NULL) < 0) { + fprintf(stderr, "Modbus plugin: Error creating mutex for modbus client node %%s\n", client_nodes[index].location); + goto error_exit; + } + client_nodes[index].init_state = 2; // we have created the mutex + + /* initialize the condition variable that will be used by the thread handling the client node */ + if (pthread_cond_init(&(client_nodes[index].condv), NULL) < 0) { + fprintf(stderr, "Modbus plugin: Error creating condition variable for modbus client node %%s\n", client_nodes[index].location); + goto error_exit; + } + client_nodes[index].execute_req = 0; //variable associated with condition variable + client_nodes[index].init_state = 3; // we have created the condition variable + + /* initialize the timer that will be used to periodically activate the client node */ + { + // start off by reseting the flag that will be set whenever the timer expires + client_nodes[index].periodic_act = 0; + + struct sigevent evp; + evp.sigev_notify = SIGEV_THREAD; /* Notification method - call a function in a new thread context */ + evp.sigev_value.sival_int = index; /* Data passed to function upon notification - used to indentify which client node to activate */ + evp.sigev_notify_function = __client_node_timer_callback_function; /* function to call upon timer expiration */ + evp.sigev_notify_attributes = NULL; /* attributes for new thread in which sigev_notify_function will be called/executed */ + + if (timer_create(CLOCK_MONOTONIC, &evp, &(client_nodes[index].timer_id)) < 0) { + fprintf(stderr, "Modbus plugin: Error creating timer for modbus client node %%s\n", client_nodes[index].location); + goto error_exit; + } + } + client_nodes[index].init_state = 4; // we have created the timer + /* launch a thread to handle this client node */ { int res = 0; @@ -450,11 +606,11 @@ res |= pthread_attr_init(&attr); res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index)); if (res != 0) { - fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location); + fprintf(stderr, "Modbus plugin: Error starting thread for modbus client node %%s\n", client_nodes[index].location); goto error_exit; } } - client_nodes[index].init_state = 2; // we have created the node and a thread + client_nodes[index].init_state = 5; // we have created the thread } /* init each local server */ @@ -499,9 +655,26 @@ int index; for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){ - /*just do the output requests */ + /* synchronize the PLC and MB buffers only for the output requests */ if (client_requests[index].req_type == req_output){ + + // lock the mutex brefore copying the data if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){ + + // Check if user configured this MB request to be activated whenever the data to be written changes + if (client_requests[index].write_on_change) { + // Let's check if the data did change... + // compare the data in plcv_buffer to coms_buffer + int res; + res = memcmp((void *)client_requests[index].coms_buffer /* buf 1 */, + (void *)client_requests[index].plcv_buffer /* buf 2*/, + REQ_BUF_SIZE * sizeof(u16) /* size in bytes */); + + // if data changed, activate execution request + if (0 != res) + client_requests[index].flag_exec_req = 1; + } + // copy from plcv_buffer to coms_buffer memcpy((void *)client_requests[index].coms_buffer /* destination */, (void *)client_requests[index].plcv_buffer /* source */, @@ -509,7 +682,33 @@ pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex)); } } - } + /* if the user program set the execution request flag, then activate the thread + * that handles this Modbus client transaction so it gets a chance to be executed + * (but don't activate the thread if it has already been activated!) + * + * NOTE that we do this, for both the IN and OUT mapped location, under this + * __publish_() function. The scan cycle of the PLC works as follows: + * - call __retrieve_() + * - execute user programs + * - call __publish_() + * - insert until time to start next periodic/cyclic scan cycle + * + * In an attempt to be able to run the MB transactions during the + * interval in which not much is going on, we handle the user program + * requests to execute a specific MB transaction in this __publish_() + * function. + */ + if ((client_requests[index].flag_exec_req != 0) && (0 == client_requests[index].flag_exec_started)) { + int client_node_id = client_requests[index].client_node_id; + if (__signal_client_thread(client_node_id) >= 0) { + /* - upon success, set flag_exec_started + * - both flags (flag_exec_req and flag_exec_started) will be reset + * once the transaction has completed. + */ + client_requests[index].flag_exec_started = 1; + } + } + } } @@ -544,12 +743,39 @@ /* kill thread and close connections of each modbus client node */ for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) { close = 0; - if (client_nodes[index].init_state >= 2) { + if (client_nodes[index].init_state >= 5) { // thread was launched, so we try to cancel it! close = pthread_cancel(client_nodes[index].thread_id); close |= pthread_join (client_nodes[index].thread_id, NULL); if (close < 0) - fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location); + fprintf(stderr, "Modbus plugin: Error closing thread for modbus client node %%s\n", client_nodes[index].location); + } + res |= close; + + close = 0; + if (client_nodes[index].init_state >= 4) { + // timer was created, so we try to destroy it! + close = timer_delete(client_nodes[index].timer_id); + if (close < 0) + fprintf(stderr, "Modbus plugin: Error destroying timer for modbus client node %%s\n", client_nodes[index].location); + } + res |= close; + + close = 0; + if (client_nodes[index].init_state >= 3) { + // condition variable was created, so we try to destroy it! + close = pthread_cond_destroy(&(client_nodes[index].condv)); + if (close < 0) + fprintf(stderr, "Modbus plugin: Error destroying condition variable for modbus client node %%s\n", client_nodes[index].location); + } + res |= close; + + close = 0; + if (client_nodes[index].init_state >= 2) { + // mutex was created, so we try to destroy it! + close = pthread_mutex_destroy(&(client_nodes[index].mutex)); + if (close < 0) + fprintf(stderr, "Modbus plugin: Error destroying mutex for modbus client node %%s\n", client_nodes[index].location); } res |= close; @@ -612,3 +838,122 @@ return res; } + + + + +/**********************************************/ +/** Functions for Beremiz web interface. **/ +/**********************************************/ + +/* + * Beremiz has a program to run on the PLC (Beremiz_service.py) + * to handle downloading of compiled programs, start/stop of PLC, etc. + * (see runtime/PLCObject.py for start/stop, loading, ...) + * + * This service also includes a web server to access PLC state (start/stop) + * and to change some basic confiuration parameters. + * (see runtime/NevowServer.py for the web server) + * + * The web server allows for extensions, where additional configuration + * parameters may be changed on the running/downloaded PLC. + * Modbus plugin also comes with an extension to the web server, through + * which the basic Modbus plugin configuration parameters may be changed + * + * These parameters are changed _after_ the code (.so file) is loaded into + * memmory. These changes may be applied before (or after) the code starts + * running (i.e. before or after __init_() ets called)! + * + * The following functions are never called from other C code. They are + * called instead from the python code in runtime/Modbus_config.py, that + * implements the web server extension for configuring Modbus parameters. + */ + + +/* The number of Cient nodes (i.e. the number of entries in the client_nodes array) + * The number of Server nodes (i.e. the numb. of entries in the server_nodes array) + * + * These variables are also used by the Modbus web config code to determine + * whether the current loaded PLC includes the Modbus plugin + * (so it should make the Modbus parameter web interface visible to the user). + */ +const int __modbus_plugin_client_node_count = NUMBER_OF_CLIENT_NODES; +const int __modbus_plugin_server_node_count = NUMBER_OF_SERVER_NODES; +const int __modbus_plugin_param_string_size = MODBUS_PARAM_STRING_SIZE; + + + +/* NOTE: We could have the python code in runtime/Modbus_config.py + * directly access the server_node_t and client_node_t structures, + * however this would create a tight coupling between these two + * disjoint pieces of code. + * Any change to the server_node_t or client_node_t structures would + * require the python code to be changed accordingly. I have therefore + * opted to create get/set functions, one for each parameter. + * + * We also convert the enumerated constants naf_ascii, etc... + * (from node_addr_family_t in modbus/mb_addr.h) + * into strings so as to decouple the python code that will be calling + * these functions from the Modbus library code definitions. + */ +const char *addr_type_str[] = { + [naf_ascii] = "ascii", + [naf_rtu ] = "rtu", + [naf_tcp ] = "tcp" +}; + + +#define __safe_strcnpy(str_dest, str_orig, max_size) { \ + strncpy(str_dest, str_orig, max_size); \ + str_dest[max_size - 1] = '\0'; \ +} + + +/* NOTE: The host, port and device parameters are strings that may be changed + * (by calling the following functions) after loading the compiled code + * (.so file) into memory, but before the code starts running + * (i.e. before __init_() gets called). + * This means that the host, port and device parameters may be changed + * _before_ they get mapped onto the str1 and str2 variables by __init_(), + * which is why the following functions must access the str1 and str2 + * parameters directly. + */ +const char * __modbus_get_ClientNode_config_name(int nodeid) {return client_nodes[nodeid].config_name; } +const char * __modbus_get_ClientNode_host (int nodeid) {return client_nodes[nodeid].str1; } +const char * __modbus_get_ClientNode_port (int nodeid) {return client_nodes[nodeid].str2; } +const char * __modbus_get_ClientNode_device (int nodeid) {return client_nodes[nodeid].str1; } +int __modbus_get_ClientNode_baud (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.baud; } +int __modbus_get_ClientNode_parity (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.parity; } +int __modbus_get_ClientNode_stop_bits (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.stop_bits;} +u64 __modbus_get_ClientNode_comm_period(int nodeid) {return client_nodes[nodeid].comm_period; } +const char * __modbus_get_ClientNode_addr_type (int nodeid) {return addr_type_str[client_nodes[nodeid].node_address.naf];} + +const char * __modbus_get_ServerNode_config_name(int nodeid) {return server_nodes[nodeid].config_name; } +const char * __modbus_get_ServerNode_host (int nodeid) {char*x=server_nodes[nodeid].str1; return (x[0]=='\0'?"#ANY#":x); } +const char * __modbus_get_ServerNode_port (int nodeid) {return server_nodes[nodeid].str2; } +const char * __modbus_get_ServerNode_device (int nodeid) {return server_nodes[nodeid].str1; } +int __modbus_get_ServerNode_baud (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.baud; } +int __modbus_get_ServerNode_parity (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.parity; } +int __modbus_get_ServerNode_stop_bits (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.stop_bits;} +u8 __modbus_get_ServerNode_slave_id (int nodeid) {return server_nodes[nodeid].slave_id; } +const char * __modbus_get_ServerNode_addr_type (int nodeid) {return addr_type_str[server_nodes[nodeid].node_address.naf];} + + +void __modbus_set_ClientNode_host (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ClientNode_port (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str2, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ClientNode_device (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ClientNode_baud (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.baud = value;} +void __modbus_set_ClientNode_parity (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.parity = value;} +void __modbus_set_ClientNode_stop_bits (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.stop_bits = value;} +void __modbus_set_ClientNode_comm_period(int nodeid, u64 value) {client_nodes[nodeid].comm_period = value;} + + +void __modbus_set_ServerNode_host (int nodeid, const char * value) {if (strcmp(value,"#ANY#")==0) value = ""; + __safe_strcnpy(server_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ServerNode_port (int nodeid, const char * value) {__safe_strcnpy(server_nodes[nodeid].str2, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ServerNode_device (int nodeid, const char * value) {__safe_strcnpy(server_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ServerNode_baud (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.baud = value;} +void __modbus_set_ServerNode_parity (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.parity = value;} +void __modbus_set_ServerNode_stop_bits (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.stop_bits = value;} +void __modbus_set_ServerNode_slave_id (int nodeid, u8 value) {server_nodes[nodeid].slave_id = value;} + diff -r 8b612b357679 -r 45f1ff6a7f87 modbus/mb_runtime.h --- a/modbus/mb_runtime.h Tue Jun 02 13:37:34 2020 +0200 +++ b/modbus/mb_runtime.h Tue Jun 16 15:53:52 2020 +0200 @@ -30,6 +30,10 @@ #define DEF_REQ_SEND_RETRIES 0 + +#define MODBUS_PARAM_STRING_SIZE 64 + + // Used by the Modbus server node #define MEM_AREA_SIZE 65536 typedef struct{ @@ -39,8 +43,48 @@ u16 rw_words[MEM_AREA_SIZE]; } server_mem_t; + +/* + * Beremiz has a program to run on the PLC (Beremiz_service.py) + * to handle downloading of compiled programs, start/stop of PLC, etc. + * (see runtime/PLCObject.py for start/stop, loading, ...) + * + * This service also includes a web server to access PLC state (start/stop) + * and to change some basic confiuration parameters. + * (see runtime/NevowServer.py for the web server) + * + * The web server allows for extensions, where additional configuration + * parameters may be changed on the running/downloaded PLC. + * Modbus plugin also comes with an extension to the web server, through + * which the basic Modbus plugin configuration parameters may be changed + * + * This means that most values in the server_node_t and client_node_t + * may be changed after the co,piled code (.so file) is loaded into + * memory, and before the code starts executing. + * Since the we will also want to change the host and port (TCP) and the + * serial device (RTU) at this time, it is best if we allocate memory for + * these strings that may be overwritten by the web server (i.e., do not use + * const strings) in the server_node_t and client_node_t structures. + * + * The following structure members + * - node_addr_t.addr.tcp.host + * - node_addr_t.addr.tcp.service (i.e. the port) + * - node_addr_t.addr.rtu.device + * are all char *, and do not allocate memory for the strings. + * + * We therefore include two generic char arrays, str1 and str2, + * that will store the above strings, and the C code will initiliaze + * the node_addre_t.addr string pointers to these strings. + * i.e., either addr.rtu.device will point to str1, + * or + * addr.tcp.host and addr.tcp.service + * will point to str1 and str2 respectively + */ typedef struct{ const char *location; + const char *config_name; + char str1[MODBUS_PARAM_STRING_SIZE]; + char str2[MODBUS_PARAM_STRING_SIZE]; u8 slave_id; node_addr_t node_address; int mb_nd; // modbus library node used for this server @@ -53,12 +97,36 @@ // Used by the Modbus client node typedef struct{ const char *location; + const char *config_name; + char str1[MODBUS_PARAM_STRING_SIZE]; + char str2[MODBUS_PARAM_STRING_SIZE]; node_addr_t node_address; - int mb_nd; + int mb_nd; // modbus library node used for this client int init_state; // store how far along the client's initialization has progressed - u64 comm_period; + u64 comm_period;// period to use when periodically sending requests to remote server int prev_error; // error code of the last printed error message (0 when no error) - pthread_t thread_id; // thread handling all communication with this client + pthread_t thread_id; // thread handling all communication for this client node + timer_t timer_id; // timer used to periodically activate this client node's thread + pthread_mutex_t mutex; // mutex to be used with the following condition variable + pthread_cond_t condv; // used to signal the client thread when to start new modbus transactions + int execute_req; /* used, in association with condition variable, + * to signal when to send the modbus request to the server + * Note that we cannot simply rely on the condition variable to signal + * when to activate the client thread, as the call to + * pthread_cond_wait() may return without having been signaled! + * From the manual: + * Spurious wakeups from the + * pthread_cond_timedwait() or pthread_cond_wait() functions may occur. + * Since the return from pthread_cond_timedwait() or pthread_cond_wait() + * does not imply anything about the value of this predicate, the predi- + * cate should be re-evaluated upon such return. + */ + int periodic_act; /* (boolen) flag will be set when the client node's thread was activated + * (by signaling the above condition variable) by the periodic timer. + * Note that this same thread may also be activated (condition variable is signaled) + * by other sources, such as when the user program requests that a specific + * client MB transation be executed (flag_exec_req in client_request_t) + */ } client_node_t; @@ -82,11 +150,37 @@ u8 error_code; // modbus error code (if any) of current request int prev_error; // error code of the last printed error message (0 when no error) struct timespec resp_timeout; + u8 write_on_change; // boolean flag. If true => execute MB request when data to send changes // buffer used to store located PLC variables u16 plcv_buffer[REQ_BUF_SIZE]; // buffer used to store data coming from / going to server u16 coms_buffer[REQ_BUF_SIZE]; pthread_mutex_t coms_buf_mutex; // mutex to access coms_buffer[] + /* boolean flag that will be mapped onto a (BOOL) located variable + * (u16 because IEC 61131-3 BOOL are mapped onto u16 in C code! ) + * -> allow PLC program to request when to start the MB transaction + * -> will be reset once the MB transaction has completed + */ + u16 flag_exec_req; + /* flag that works in conjunction with flag_exec_req + * (does not really need to be u16 as it is not mapped onto a located variable. ) + * -> used by internal logic to indicate that the client thread + * that will be executing the MB transaction + * requested by flag exec_req has already been activated. + * -> will be reset once the MB transaction has completed + */ + u16 flag_exec_started; + /* flag that will be mapped onto a (WORD) located variable + * (u16 because the flag is a word! ) + * -> MSByte will store the result of the last executed MB transaction + * 1 -> error accessing IP network, or serial interface + * 2 -> reply received from server was an invalid frame + * 3 -> server did not reply before timeout expired + * 4 -> server returned a valid error frame + * -> if the MSByte is 4, the LSByte will store the MB error code returned by the server + * -> will be reset (set to 0) once this MB transaction has completed sucesfully + */ + u16 flag_exec_status; } client_request_t; diff -r 8b612b357679 -r 45f1ff6a7f87 modbus/mb_utils.py --- a/modbus/mb_utils.py Tue Jun 02 13:37:34 2020 +0200 +++ b/modbus/mb_utils.py Tue Jun 16 15:53:52 2020 +0200 @@ -57,20 +57,19 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", %(slaveid)s, {naf_tcp, {.tcp = {%(host)s, "%(port)s", DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - host, port, slaveid = GetCTVals(child, range(3)) +{"%(locnodestr)s", "%(config_name)s", "%(host)s", "%(port)s", %(slaveid)s, {naf_tcp, {.tcp = {NULL, NULL, DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, host, port, slaveid = GetCTVals(child, range(4)) if host == "#ANY#": - host = 'INADDR_ANY' - else: - host = '"' + host + '"' + host = '' # slaveid = GetCTVal(child, 2) # if int(slaveid) not in xrange(256): # self.GetCTRoot().logger.write_error("Error: Wrong slave ID in %s server node\nModbus Plugin C code returns empty\n"%location) # return None node_dict = {"locnodestr": location, + "config_name": config_name, "host": host, "port": port, "slaveid": slaveid} @@ -96,7 +95,7 @@ "Modbus plugin: Invalid Start Address in server memory area node %(locreqstr)s (Must be in the range [0..65535])\nModbus plugin: Aborting C code generation for this node\n" % request_dict) return None request_dict["count"] = GetCTVal(child, 1) - if int(request_dict["count"]) not in xrange(1, 65536): + if int(request_dict["count"]) not in xrange(1, 65537): self.GetCTRoot().logger.write_error( "Modbus plugin: Invalid number of channels in server memory area node %(locreqstr)s (Must be in the range [1..65536-start_address])\nModbus plugin: Aborting C code generation for this node\n" % request_dict) return None @@ -120,12 +119,13 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", %(slaveid)s, {naf_rtu, {.rtu = {"%(device)s", %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - device, baud, parity, stopbits, slaveid = GetCTVals(child, range(5)) - - node_dict = {"locnodestr": location, +{"%(locnodestr)s", "%(config_name)s", "%(device)s", "",%(slaveid)s, {naf_rtu, {.rtu = {NULL, %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, device, baud, parity, stopbits, slaveid = GetCTVals(child, range(6)) + + node_dict = {"locnodestr": location, + "config_name": config_name, "device": device, "baud": baud, "parity": modbus_serial_parity_dict[parity], @@ -140,12 +140,13 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", {naf_rtu, {.rtu = {"%(device)s", %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - device, baud, parity, stopbits, coms_period = GetCTVals(child, range(5)) - - node_dict = {"locnodestr": location, +{"%(locnodestr)s", "%(config_name)s", "%(device)s", "", {naf_rtu, {.rtu = {NULL, %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, device, baud, parity, stopbits, coms_period = GetCTVals(child, range(6)) + + node_dict = {"locnodestr": location, + "config_name": config_name, "device": device, "baud": baud, "parity": modbus_serial_parity_dict[parity], @@ -160,12 +161,13 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", {naf_tcp, {.tcp = {"%(host)s", "%(port)s", DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */, 0 /* prev_error */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - host, port, coms_period = GetCTVals(child, range(3)) - - node_dict = {"locnodestr": location, +{"%(locnodestr)s", "%(config_name)s", "%(host)s", "%(port)s", {naf_tcp, {.tcp = {NULL, NULL, DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */, 0 /* prev_error */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, host, port, coms_period = GetCTVals(child, range(4)) + + node_dict = {"locnodestr": location, + "config_name": config_name, "host": host, "port": port, "coms_period": coms_period} @@ -184,7 +186,7 @@ req_init_template = '''/*request %(locreqstr)s*/ {"%(locreqstr)s", %(nodeid)s, %(slaveid)s, %(iotype)s, %(func_nr)s, %(address)s , %(count)s, -DEF_REQ_SEND_RETRIES, 0 /* error_code */, 0 /* prev_code */, {%(timeout_s)d, %(timeout_ns)d} /* timeout */, +DEF_REQ_SEND_RETRIES, 0 /* error_code */, 0 /* prev_code */, {%(timeout_s)d, %(timeout_ns)d} /* timeout */, %(write_on_change)d /* write_on_change */, {%(buffer)s}, {%(buffer)s}}''' timeout = int(GetCTVal(child, 4)) @@ -198,6 +200,7 @@ "slaveid": GetCTVal(child, 1), "address": GetCTVal(child, 3), "count": GetCTVal(child, 2), + "write_on_change": GetCTVal(child, 5), "timeout": timeout, "timeout_s": timeout_s, "timeout_ns": timeout_ns, @@ -222,5 +225,11 @@ self.GetCTRoot().logger.write_error( "Modbus plugin: Invalid number of channels in TCP client request node %(locreqstr)s (start_address + nr_channels must be less than 65536)\nModbus plugin: Aborting C code generation for this node\n" % request_dict) return None + if (request_dict["write_on_change"] and (request_dict["iotype"] == 'req_input')): + self.GetCTRoot().logger.write_error( + "Modbus plugin: (warning) MB client request node %(locreqstr)s has option 'write_on_change' enabled.\nModbus plugin: This option will be ignored by the Modbus read function.\n" % request_dict) + # NOTE: this is only a warning (we don't wish to abort code generation) so following line must be left commented out! + # return None + return req_init_template % request_dict diff -r 8b612b357679 -r 45f1ff6a7f87 modbus/modbus.py --- a/modbus/modbus.py Tue Jun 02 13:37:34 2020 +0200 +++ b/modbus/modbus.py Tue Jun 16 15:53:52 2020 +0200 @@ -30,6 +30,7 @@ from modbus.mb_utils import * from ConfigTreeNode import ConfigTreeNode from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY +import util.paths as paths base_folder = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] base_folder = os.path.join(base_folder, "..") @@ -83,6 +84,7 @@ + @@ -115,7 +117,29 @@ datatacc = modbus_function_dict[function][6] # 'Coil', 'Holding Register', 'Input Discrete' or 'Input Register' dataname = modbus_function_dict[function][7] + # start off with a boolean entry + # This is a flag used to allow the user program to control when to + # execute the Modbus request. + # NOTE: If the Modbus request has a 'current_location' of + # %QX1.2.3 + # then the execution control flag will be + # %QX1.2.3.0.0 + # and all the Modbus registers/coils will be + # %QX1.2.3.0 + # %QX1.2.3.1 + # %QX1.2.3.2 + # .. + # %QX1.2.3.n entries = [] + entries.append({ + "name": "Exec. request flag", + "type": LOCATION_VAR_MEMORY, + "size": 1, # BOOL flag + "IEC_type": "BOOL", # BOOL flag + "var_name": "var_name", + "location": "X" + ".".join([str(i) for i in current_location]) + ".0.0", + "description": "MB request execution control flag", + "children": []}) for offset in range(address, address + count): entries.append({ "name": dataname + " " + str(offset), @@ -260,17 +284,20 @@ # # +# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique + class _ModbusTCPclientPlug(object): XSD = """ + - + @@ -285,11 +312,33 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusTCPclient" + + def __init__(self): + # NOTE: + # The ModbusTCPclient attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusTCPclient.setConfiguration_Name("Modbus TCP Client " + loc_str) + # Return the number of (modbus library) nodes this specific TCP client will need # return type: (tcp nodes, rtu nodes, ascii nodes) def GetNodeCount(self): return (1, 0, 0) + def GetConfigName(self): + """ Return the node's Configuration_Name """ + return self.ModbusTCPclient.getConfiguration_Name() + def CTNGenerate_C(self, buildpath, locations): """ Generate C code @@ -314,6 +363,8 @@ # # +# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique + class _ModbusTCPserverPlug(object): # NOTE: the Port number is a 'string' and not an 'integer'! # This is because the underlying modbus library accepts strings @@ -322,6 +373,7 @@ + @@ -340,17 +392,41 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusTCPserver" + def __init__(self): + # NOTE: + # The ModbusServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusServerNode.setConfiguration_Name("Modbus TCP Server " + loc_str) + # Return the number of (modbus library) nodes this specific TCP server will need # return type: (tcp nodes, rtu nodes, ascii nodes) def GetNodeCount(self): return (1, 0, 0) - # Return a list with a single tuple conatining the (location, port number) - # location: location of this node in the configuration tree + # Return a list with a single tuple conatining the (location, IP address, port number) + # location : location of this node in the configuration tree # port number: IP port used by this Modbus/IP server + # IP address : IP address of the network interface on which the server will be listening + # ("", "*", or "#ANY#" => listening on all interfaces!) def GetIPServerPortNumbers(self): - port = self.GetParamsAttributes()[0]["children"][1]["value"] - return [(self.GetCurrentLocation(), port)] + port = self.ModbusServerNode.getLocal_Port_Number() + addr = self.ModbusServerNode.getLocal_IP_Address() + return [(self.GetCurrentLocation(), addr, port)] + + def GetConfigName(self): + """ Return the node's Configuration_Name """ + return self.ModbusServerNode.getConfiguration_Name() def CTNGenerate_C(self, buildpath, locations): """ @@ -376,11 +452,14 @@ # # +# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique + class _ModbusRTUclientPlug(object): XSD = """ + @@ -388,7 +467,7 @@ - + @@ -403,6 +482,23 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusRTUclient" + def __init__(self): + # NOTE: + # The ModbusRTUclient attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusRTUclient.setConfiguration_Name("Modbus RTU Client " + loc_str) + def GetParamsAttributes(self, path=None): infos = ConfigTreeNode.GetParamsAttributes(self, path=path) for element in infos: @@ -421,6 +517,10 @@ def GetNodeCount(self): return (0, 1, 0) + def GetConfigName(self): + """ Return the node's Configuration_Name """ + return self.ModbusRTUclient.getConfiguration_Name() + def CTNGenerate_C(self, buildpath, locations): """ Generate C code @@ -445,12 +545,14 @@ # # +# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique class _ModbusRTUslavePlug(object): XSD = """ + @@ -471,6 +573,23 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusRTUslave" + def __init__(self): + # NOTE: + # The ModbusRTUslave attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusRTUslave.setConfiguration_Name("Modbus RTU Slave " + loc_str) + def GetParamsAttributes(self, path=None): infos = ConfigTreeNode.GetParamsAttributes(self, path=path) for element in infos: @@ -489,6 +608,10 @@ def GetNodeCount(self): return (0, 1, 0) + def GetConfigName(self): + """ Return the node's Configuration_Name """ + return self.ModbusRTUslave.getConfiguration_Name() + def CTNGenerate_C(self, buildpath, locations): """ Generate C code @@ -550,8 +673,7 @@ x1 + x2 for x1, x2 in zip(total_node_count, child.GetNodeCount())) return total_node_count - # Return a list with tuples of the (location, port numbers) used by all - # the Modbus/IP servers + # Return a list with tuples of the (location, port numbers) used by all the Modbus/IP servers def GetIPServerPortNumbers(self): IPServer_port_numbers = [] for child in self.IECSortedChildren(): @@ -559,6 +681,13 @@ IPServer_port_numbers.extend(child.GetIPServerPortNumbers()) return IPServer_port_numbers + # Return a list with tuples of the (location, configuration_name) used by all the Modbus nodes (tcp/rtu, clients/servers) + def GetConfigNames(self): + Node_Configuration_Names = [] + for child in self.IECSortedChildren(): + Node_Configuration_Names.extend([(child.GetCurrentLocation(), child.GetConfigName())]) + return Node_Configuration_Names + def CTNGenerate_C(self, buildpath, locations): # print "#############" # print self.__class__ @@ -573,40 +702,61 @@ # Determine the number of (modbus library) nodes ALL instances of the modbus plugin will need # total_node_count: (tcp nodes, rtu nodes, ascii nodes) - # Also get a list with tuples of (location, IP port numbers) used by all the Modbus/IP server nodes + # + # Also get a list with tuples of (location, IP address, port number) used by all the Modbus/IP server nodes # This list is later used to search for duplicates in port numbers! - # IPServer_port_numbers = [(location ,IPserver_port_number), ...] - # location: tuple similar to (0, 3, 1) representing the location in the configuration tree "0.3.1.x" - # IPserver_port_number: a number (i.e. port number used by the - # Modbus/IP server) + # IPServer_port_numbers = [(location, IP address, port number), ...] + # location : tuple similar to (0, 3, 1) representing the location in the configuration tree "0.3.1.x" + # IPserver_port_number: a number (i.e. port number used by the Modbus/IP server) + # IP address : IP address of the network interface on which the server will be listening + # ("", "*", or "#ANY#" => listening on all interfaces!) + # + # Also get a list with tuples of (location, Configuration_Name) used by all the Modbus nodes + # This list is later used to search for duplicates in Configuration Names! + # Node_Configuration_Names = [(location, Configuration_Name), ...] + # location : tuple similar to (0, 3, 1) representing the location in the configuration tree "0.3.1.x" + # Configuration_Name: the "Configuration_Name" string total_node_count = (0, 0, 0) - IPServer_port_numbers = [] + IPServer_port_numbers = [] + Node_Configuration_Names = [] for CTNInstance in self.GetCTRoot().IterChildren(): if CTNInstance.CTNType == "modbus": - # ask each modbus plugin instance how many nodes it needs, and - # add them all up. - total_node_count = tuple(x1 + x2 for x1, x2 in zip( - total_node_count, CTNInstance.GetNodeCount())) - IPServer_port_numbers.extend( - CTNInstance.GetIPServerPortNumbers()) + # ask each modbus plugin instance how many nodes it needs, and add them all up. + total_node_count = tuple(x1 + x2 for x1, x2 in zip(total_node_count, CTNInstance.GetNodeCount())) + IPServer_port_numbers. extend(CTNInstance.GetIPServerPortNumbers()) + Node_Configuration_Names.extend(CTNInstance.GetConfigNames ()) + + # Search for use of duplicate Configuration_Names by Modbus nodes + # Configuration Names are used by the web server running on the PLC + # (more precisely, run by Beremiz_service.py) to identify and allow + # changing the Modbus parameters after the program has been downloaded + # to the PLC (but before it is started) + # With clashes in the configuration names, the Modbus nodes will not be + # distinguasheble on the web interface! + for i in range(0, len(Node_Configuration_Names) - 1): + for j in range(i + 1, len(Node_Configuration_Names)): + if Node_Configuration_Names[i][1] == Node_Configuration_Names[j][1]: + error_message = _("Error: Modbus plugin nodes %{a1}.x and %{a2}.x use the same Configuration_Name \"{a3}\".\n").format( + a1=_lt_to_str(Node_Configuration_Names[i][0]), + a2=_lt_to_str(Node_Configuration_Names[j][0]), + a3=Node_Configuration_Names[j][1]) + self.FatalError(error_message) # Search for use of duplicate port numbers by Modbus/IP servers - # print IPServer_port_numbers - # ..but first define a lambda function to convert a tuple with the config tree location to a nice looking string - # for e.g., convert the tuple (0, 3, 4) to "0.3.4" - - for i in range(0, len(IPServer_port_numbers) - 1): - for j in range(i + 1, len(IPServer_port_numbers)): - if IPServer_port_numbers[i][1] == IPServer_port_numbers[j][1]: - self.GetCTRoot().logger.write_warning( - _("Error: Modbus/IP Servers %{a1}.x and %{a2}.x use the same port number {a3}.\n"). - format( - a1=_lt_to_str(IPServer_port_numbers[i][0]), - a2=_lt_to_str(IPServer_port_numbers[j][0]), - a3=IPServer_port_numbers[j][1])) - raise Exception - # TODO: return an error code instead of raising an - # exception + # Note: We only consider duplicate port numbers if using the same network interface! + i = 0 + for loc1, addr1, port1 in IPServer_port_numbers[:-1]: + i = i + 1 + for loc2, addr2, port2 in IPServer_port_numbers[i:]: + if (port1 == port2) and ( + (addr1 == addr2) # on the same network interface + or (addr1 == "") or (addr1 == "*") or (addr1 == "#ANY#") # or one (or both) of the servers + or (addr2 == "") or (addr2 == "*") or (addr2 == "#ANY#") # use all available network interfaces + ): + error_message = _("Error: Modbus plugin nodes %{a1}.x and %{a2}.x use same port number \"{a3}\" " + + "on the same (or overlapping) network interfaces \"{a4}\" and \"{a5}\".\n").format( + a1=_lt_to_str(loc1), a2=_lt_to_str(loc2), a3=port1, a4=addr1, a5=addr2) + self.FatalError(error_message) # Determine the current location in Beremiz's project configuration # tree @@ -720,12 +870,32 @@ for iecvar in subchild.GetLocations(): # absloute address - start address relative_addr = iecvar["LOC"][3] - int(GetCTVal(subchild, 3)) - # test if relative address in request specified range - if relative_addr in xrange(int(GetCTVal(subchild, 2))): + # test if the located variable + # (a) has relative address in request specified range + # AND is NOT + # (b) is a control flag added by this modbus plugin + # to control its execution at runtime. + # Currently, we only add the "Execution Control Flag" + # to each client request (one flag per request) + # to control when to execute the request (if not executed periodically) + # While all Modbus registers/coils are mapped onto a location + # with 4 numbers (e.g. %QX0.1.2.55), this control flag is mapped + # onto a location with 4 numbers (e.g. %QX0.1.2.0.0), where the last + # two numbers are always '0.0', and the first two identify the request. + # In the following if, we check for this condition by checking + # if their are at least 4 or more number in the location's address. + if ( relative_addr in xrange(int(GetCTVal(subchild, 2))) # condition (a) explained above + and len(iecvar["LOC"]) < 5): # condition (b) explained above if str(iecvar["NAME"]) not in loc_vars_list: loc_vars.append( "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].plcv_buffer[%d];" % (client_requestid, relative_addr)) loc_vars_list.append(str(iecvar["NAME"])) + # Now add the located variable in case it is a flag (condition (b) above + if len(iecvar["LOC"]) >= 5: # condition (b) explained above + if str(iecvar["NAME"]) not in loc_vars_list: + loc_vars.append( + "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].flag_exec_req;" % (client_requestid)) + loc_vars_list.append(str(iecvar["NAME"])) client_requestid += 1 tcpclient_node_count += 1 client_nodeid += 1 @@ -745,12 +915,32 @@ for iecvar in subchild.GetLocations(): # absloute address - start address relative_addr = iecvar["LOC"][3] - int(GetCTVal(subchild, 3)) - # test if relative address in request specified range - if relative_addr in xrange(int(GetCTVal(subchild, 2))): + # test if the located variable + # (a) has relative address in request specified range + # AND is NOT + # (b) is a control flag added by this modbus plugin + # to control its execution at runtime. + # Currently, we only add the "Execution Control Flag" + # to each client request (one flag per request) + # to control when to execute the request (if not executed periodically) + # While all Modbus registers/coils are mapped onto a location + # with 4 numbers (e.g. %QX0.1.2.55), this control flag is mapped + # onto a location with 4 numbers (e.g. %QX0.1.2.0.0), where the last + # two numbers are always '0.0', and the first two identify the request. + # In the following if, we check for this condition by checking + # if their are at least 4 or more number in the location's address. + if ( relative_addr in xrange(int(GetCTVal(subchild, 2))) # condition (a) explained above + and len(iecvar["LOC"]) < 5): # condition (b) explained above if str(iecvar["NAME"]) not in loc_vars_list: loc_vars.append( "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].plcv_buffer[%d];" % (client_requestid, relative_addr)) loc_vars_list.append(str(iecvar["NAME"])) + # Now add the located variable in case it is a flag (condition (b) above + if len(iecvar["LOC"]) >= 5: # condition (b) explained above + if str(iecvar["NAME"]) not in loc_vars_list: + loc_vars.append( + "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].flag_exec_req;" % (client_requestid)) + loc_vars_list.append(str(iecvar["NAME"])) client_requestid += 1 rtuclient_node_count += 1 client_nodeid += 1 @@ -803,4 +993,18 @@ # LDFLAGS.append(" -lws2_32 ") # on windows we need to load winsock # library! - return [(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True + websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r') + websettingcode = websettingfile.read() + websettingfile.close() + + location_str = "_".join(map(str, self.GetCurrentLocation())) + websettingcode = websettingcode % locals() + + runtimefile_path = os.path.join(buildpath, "runtime_modbus_websettings.py") + runtimefile = open(runtimefile_path, 'w') + runtimefile.write(websettingcode) + runtimefile.close() + + return ([(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True, + ("runtime_modbus_websettings_%s.py" % location_str, open(runtimefile_path, "rb")), + ) diff -r 8b612b357679 -r 45f1ff6a7f87 modbus/web_settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modbus/web_settings.py Tue Jun 16 15:53:52 2020 +0200 @@ -0,0 +1,628 @@ +#!/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 + + + + +############################################################################################## +# This file implements an extension to the web server embedded in the Beremiz_service.py # +# runtime manager (webserver is in runtime/NevowServer.py). # +# # +# The extension implemented in this file allows for runtime configuration # +# of Modbus plugin parameters # +############################################################################################## + + + +import json +import os +import ctypes +import string +import hashlib + +from formless import annotate, webform + +import runtime.NevowServer as NS + +# Directory in which to store the persistent configurations +# Should be a directory that does not get wiped on reboot! +_ModbusConfFiledir = WorkingDir + +# List of all Web Extension Setting nodes we are handling. +# One WebNode each for: +# - Modbus TCP client +# - Modbus TCP server +# - Modbus RTU client +# - Modbus RTU slave +# configured in the loaded PLC (i.e. the .so file loaded into memory) +# Each entry will be a dictionary. See _AddWebNode() for the details +# of the data structure in each entry. +_WebNodeList = [] + + + + +class MB_StrippedString(annotate.String): + def __init__(self, *args, **kwargs): + annotate.String.__init__(self, strip = True, *args, **kwargs) + + +class MB_StopBits(annotate.Choice): + _choices = [0, 1, 2] + + def coerce(self, val, configurable): + return int(val) + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) + + +class MB_Baud(annotate.Choice): + _choices = [110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200] + + def coerce(self, val, configurable): + return int(val) + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) + + +class MB_Parity(annotate.Choice): + # For more info on what this class really does, have a look at the code in + # file twisted/nevow/annotate.py + # grab this code from $git clone https://github.com/twisted/nevow/ + # + # Warning: do _not_ name this variable choice[] without underscore, as that name is + # already used for another similar variable by the underlying class annotate.Choice + _choices = [ 0, 1, 2 ] + _label = ["none", "odd", "even"] + + def choice_to_label(self, key): + #PLCObject.LogMessage("Modbus web server extension::choice_to_label() " + str(key)) + return self._label[key] + + def coerce(self, val, configurable): + """Coerce a value with the help of an object, which is the object + we are configuring. + """ + # Basically, make sure the value the user introduced is valid, and transform + # into something that is valid if necessary or mark it as an error + # (by raising an exception ??). + # + # We are simply using this functions to transform the input value (a string) + # into an integer. Note that although the available options are all + # integers (0, 1 or 2), even though what is shown on the user interface + # are actually strings, i.e. the labels), these parameters are for some + # reason being parsed as strings, so we need to map them back to an + # integer. + # + #PLCObject.LogMessage("Modbus web server extension::coerce " + val ) + return int(val) + + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, + choices = self._choices, + stringify = self.choice_to_label, + *args, **kwargs) + + + +# Parameters we will need to get from the C code, but that will not be shown +# on the web interface. Common to all modbus entry types (client/server, tcp/rtu/ascii) +# +# The annotate type entry is basically useless and is completely ignored. +# We kee that entry so that this list can later be correctly merged with the +# following lists... +General_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, ...) + ("config_name" , _("") , ctypes.c_char_p, annotate.String), + ("addr_type" , _("") , ctypes.c_char_p, annotate.String) + ] + +# Parameters we will need to get from the C code, and that _will_ be shown +# on the web interface. +TCPclient_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, ...) + ("host" , _("Remote IP Address") , ctypes.c_char_p, MB_StrippedString), + ("port" , _("Remote Port Number") , ctypes.c_char_p, MB_StrippedString), + ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer ) + ] + +RTUclient_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, ...) + ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString), + ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ), + ("parity" , _("Parity") , ctypes.c_int, MB_Parity ), + ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ), + ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer) + ] + +TCPserver_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, ...) + ("host" , _("Local IP Address") , ctypes.c_char_p, MB_StrippedString), + ("port" , _("Local Port Number") , ctypes.c_char_p, MB_StrippedString), + ("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer ) + ] + +RTUslave_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, ...) + ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString), + ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ), + ("parity" , _("Parity") , ctypes.c_int, MB_Parity ), + ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ), + ("slave_id" , _("Slave ID") , ctypes.c_ulonglong, annotate.Integer) + ] + + + + +# Dictionary containing List of Web viewable parameters +# Note: the dictionary key must be the same as the string returned by the +# __modbus_get_ClientNode_addr_type() +# __modbus_get_ServerNode_addr_type() +# functions implemented in C (see modbus/mb_runtime.c) +_client_WebParamListDict = {} +_client_WebParamListDict["tcp" ] = TCPclient_parameters +_client_WebParamListDict["rtu" ] = RTUclient_parameters +_client_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) + +_server_WebParamListDict = {} +_server_WebParamListDict["tcp" ] = TCPserver_parameters +_server_WebParamListDict["rtu" ] = RTUslave_parameters +_server_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) + +WebParamListDictDict = {} +WebParamListDictDict['client'] = _client_WebParamListDict +WebParamListDictDict['server'] = _server_WebParamListDict + + + + + + +def _SetSavedConfiguration(WebNode_id, newConfig): + """ Stores a dictionary in a persistant file containing the Modbus parameter configuration """ + WebNode_entry = _WebNodeList[WebNode_id] + + if WebNode_entry["DefaultConfiguration"] == newConfig: + + _DelSavedConfiguration(WebNode_id) + WebNode_entry["SavedConfiguration"] = None + + else: + + # Add the addr_type and node_type to the data that will be saved to file + # This allows us to confirm the saved data contains the correct addr_type + # when loading from file + save_info = {} + save_info["addr_type"] = ["addr_type"] + save_info["node_type"] = WebNode_entry["node_type"] + save_info["config" ] = newConfig + + filename = WebNode_entry["filename"] + + with open(os.path.realpath(filename), 'w') as f: + json.dump(save_info, f, sort_keys=True, indent=4) + + WebNode_entry["SavedConfiguration"] = newConfig + + + + +def _DelSavedConfiguration(WebNode_id): + """ Deletes the file cotaining the persistent Modbus configuration """ + filename = _WebNodeList[WebNode_id]["filename"] + + if os.path.exists(filename): + os.remove(filename) + + + + +def _GetSavedConfiguration(WebNode_id): + """ + Returns a dictionary containing the Modbus parameter configuration + that was last saved to file. If no file exists, or file contains + wrong addr_type (i.e. 'tcp', 'rtu' or 'ascii' -> does not match the + addr_type of the WebNode_id), then return None + """ + filename = _WebNodeList[WebNode_id]["filename"] + try: + #if os.path.isfile(filename): + save_info = json.load(open(filename)) + except Exception: + return None + + if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]: + return None + if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]: + return None + if "config" not in save_info: + return None + + saved_config = save_info["config"] + + #if _CheckConfiguration(saved_config): + # return saved_config + #else: + # return None + + return saved_config + + + +def _GetPLCConfiguration(WebNode_id): + """ + Returns a dictionary containing the current Modbus parameter configuration + stored in the C variables in the loaded PLC (.so file) + """ + current_config = {} + C_node_id = _WebNodeList[WebNode_id]["C_node_id"] + WebParamList = _WebNodeList[WebNode_id]["WebParamList"] + GetParamFuncs = _WebNodeList[WebNode_id]["GetParamFuncs"] + + for par_name, x1, x2, x3 in WebParamList: + value = GetParamFuncs[par_name](C_node_id) + if value is not None: + current_config[par_name] = value + + return current_config + + + +def _SetPLCConfiguration(WebNode_id, newconfig): + """ + Stores the Modbus parameter configuration into the + the C variables in the loaded PLC (.so file) + """ + C_node_id = _WebNodeList[WebNode_id]["C_node_id"] + SetParamFuncs = _WebNodeList[WebNode_id]["SetParamFuncs"] + + for par_name in newconfig: + value = newconfig[par_name] + if value is not None: + SetParamFuncs[par_name](C_node_id, value) + + + + +def _GetWebviewConfigurationValue(ctx, WebNode_id, argument): + """ + Callback function, called by the web interface (NevowServer.py) + to fill in the default value of each parameter of the web form + + Note that the real callback function is a dynamically created function that + will simply call this function to do the work. It will also pass the WebNode_id + as a parameter. + """ + try: + return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name] + except Exception: + return "" + + + +def OnButtonSave(**kwargs): + """ + Function called when user clicks 'Save' button in web interface + The function will configure the Modbus plugin in the PLC with the values + specified in the web interface. However, values must be validated first! + + Note that this function does not get called directly. The real callback + function is the dynamic __OnButtonSave() function, which will add the + "WebNode_id" argument, and call this function to do the work. + """ + + #PLCObject.LogMessage("Modbus web server extension::OnButtonSave() Called") + + newConfig = {} + WebNode_id = kwargs.get("WebNode_id", None) + WebParamList = _WebNodeList[WebNode_id]["WebParamList"] + + for par_name, x1, x2, x3 in WebParamList: + value = kwargs.get(par_name, None) + if value is not None: + newConfig[par_name] = value + + # First check if configuration is OK. + # Note that this is not currently required, as we use drop down choice menus + # for baud, parity and sop bits, so the values should always be correct! + #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(WebNode_id, newConfig) + + # Configure PLC with the current Modbus parameters + _SetPLCConfiguration(WebNode_id, newConfig) + + # Update the viewable configuration + # The PLC may have coerced the values on calling _SetPLCConfiguration() + # so we do not set it directly to newConfig + _WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetPLCConfiguration(WebNode_id) + + + +def OnButtonReset(**kwargs): + """ + Function called when user clicks 'Delete' button in web interface + The function will delete the file containing the persistent + Modbus configution + """ + + WebNode_id = kwargs.get("WebNode_id", None) + + # Delete the file + _DelSavedConfiguration(WebNode_id) + + # Set the current configuration to the default (hardcoded in C) + new_config = _WebNodeList[WebNode_id]["DefaultConfiguration"] + _SetPLCConfiguration(WebNode_id, new_config) + + #Update the webviewconfiguration + _WebNodeList[WebNode_id]["WebviewConfiguration"] = new_config + + # Reset SavedConfiguration + _WebNodeList[WebNode_id]["SavedConfiguration"] = None + + + + + +def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs): + """ + Load from the compiled code (.so file, aloready loaded into memmory) + the configuration parameters of a specific Modbus plugin node. + This function works with both client and server nodes, depending on the + Get/SetParamFunc dictionaries passed to it (either the client or the server + node versions of the Get/Set functions) + """ + WebNode_entry = {} + + # Get the config_name from the C code... + config_name = GetParamFuncs["config_name"](C_node_id) + # Get the addr_type from the C code... + # addr_type will be one of "tcp", "rtu" or "ascii" + addr_type = GetParamFuncs["addr_type" ](C_node_id) + # For some operations we cannot use the config name (e.g. filename to store config) + # because the user may be using characters that are invalid for that purpose ('/' for + # example), so we create a hash of the config_name, and use that instead. + config_hash = hashlib.md5(config_name).hexdigest() + + #PLCObject.LogMessage("Modbus web server extension::_AddWebNode("+str(C_node_id)+") config_name="+config_name) + + # Add the new entry to the global list + # Note: it is OK, and actually necessary, to do this _before_ seting all the parameters in WebNode_entry + # WebNode_entry will be stored as a reference, so we can later insert parameters at will. + global _WebNodeList + _WebNodeList.append(WebNode_entry) + WebNode_id = len(_WebNodeList) - 1 + + # store all WebNode relevant data for future reference + # + # Note that "WebParamList" will reference one of: + # - TCPclient_parameters, TCPserver_parameters, RTUclient_parameters, RTUslave_parameters + WebNode_entry["C_node_id" ] = C_node_id + WebNode_entry["config_name" ] = config_name + WebNode_entry["config_hash" ] = config_hash + WebNode_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json") + WebNode_entry["GetParamFuncs"] = GetParamFuncs + WebNode_entry["SetParamFuncs"] = SetParamFuncs + WebNode_entry["WebParamList" ] = WebParamListDictDict[node_type][addr_type] + WebNode_entry["addr_type" ] = addr_type # 'tcp', 'rtu', or 'ascii' (as returned by C function) + WebNode_entry["node_type" ] = node_type # 'client', 'server' + + + # Dictionary that contains the Modbus 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 an edited configuration that contains an error. + WebNode_entry["WebviewConfiguration"] = None + + # Upon PLC load, this Dictionary is initialised with the Modbus configuration + # hardcoded in the C file + # (i.e. the configuration inserted in Beremiz IDE when project was compiled) + WebNode_entry["DefaultConfiguration"] = _GetPLCConfiguration(WebNode_id) + WebNode_entry["WebviewConfiguration"] = WebNode_entry["DefaultConfiguration"] + + # Dictionary that stores the Modbus 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) + SavedConfig = _GetSavedConfiguration(WebNode_id) + WebNode_entry["SavedConfiguration"] = SavedConfig + + if SavedConfig is not None: + _SetPLCConfiguration(WebNode_id, SavedConfig) + WebNode_entry["WebviewConfiguration"] = SavedConfig + + # Define the format for the web form used to show/change the current parameters + # We first declare a dynamic function to work as callback to obtain the default values for each parameter + # Note: We transform every parameter into a string + # This is not strictly required for parameters of type annotate.Integer that will correctly + # accept the default value as an Integer python object + # This is obviously also not required for parameters of type annotate.String, that are + # always handled as strings. + # However, the annotate.Choice parameters (and all parameters that derive from it, + # sucn as Parity, Baud, etc.) require the default value as a string + # even though we store it as an integer, which is the data type expected + # by the set_***() C functions in mb_runtime.c + def __GetWebviewConfigurationValue(ctx, argument): + return str(_GetWebviewConfigurationValue(ctx, WebNode_id, argument)) + + webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue)) + for name, web_label, c_dtype, web_dtype in WebNode_entry["WebParamList"]] + + # Configure the web interface to include the Modbus config parameters + def __OnButtonSave(**kwargs): + OnButtonSave(WebNode_id=WebNode_id, **kwargs) + + WebSettings = NS.newExtensionSetting("Modbus #"+ str(WebNode_id), config_hash) + + WebSettings.addSettings( + "ModbusConfigParm" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + webFormInterface, # fields + _("Apply"), # button label + __OnButtonSave) # callback + + def __OnButtonReset(**kwargs): + return OnButtonReset(WebNode_id = WebNode_id, **kwargs) + + def getConfigStatus(): + if WebNode_entry["WebviewConfiguration"] == WebNode_entry["DefaultConfiguration"]: + return "Unchanged" + return "Modified" + + WebSettings.addSettings( + "ModbusConfigDelSaved" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + [ ("status", + annotate.String(label=_("Current state"), + immutable=True, + default=lambda *k:getConfigStatus())), + ], # fields (empty, no parameters required!) + _("Reset"), # button label + __OnButtonReset) + + + + +def _runtime_modbus_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 loaded into memory + """ + + #PLCObject.LogMessage("Modbus 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 + + # Get the number of Modbus Client and Servers (Modbus plugin) + # configured in the currently loaded PLC project (i.e., the .so file) + # If the "__modbus_plugin_client_node_count" + # or the "__modbus_plugin_server_node_count" C variables + # are not present in the .so file we conclude that the currently loaded + # PLC does not have the Modbus plugin included (situation (2b) described above init()) + try: + # XXX TODO : stop reading from PLC .so file. This code is template code + # that can use modbus extension build data, such as client node count. + client_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_client_node_count").value + server_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_server_node_count").value + except Exception: + # Loaded PLC does not have the Modbus plugin => nothing to do + # (i.e. do _not_ configure and make available the Modbus web interface) + return + + if client_count < 0: client_count = 0 + if server_count < 0: server_count = 0 + + if (client_count == 0) and (server_count == 0): + # The Modbus plugin in the loaded PLC does not have any client and servers configured + # => nothing to do (i.e. do _not_ configure and make available the Modbus web interface) + return + + # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters + # Will contain references to the C functions (implemented in beremiz/modbus/mb_runtime.c) + GetClientParamFuncs = {} + SetClientParamFuncs = {} + GetServerParamFuncs = {} + SetServerParamFuncs = {} + + # XXX TODO : stop reading from PLC .so file. This code is template code + # that can use modbus extension build data + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters: + ParamFuncName = "__modbus_get_ClientNode_" + name + GetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + GetClientParamFuncs[name].restype = c_dtype + GetClientParamFuncs[name].argtypes = [ctypes.c_int] + + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters: + ParamFuncName = "__modbus_set_ClientNode_" + name + SetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + SetClientParamFuncs[name].restype = None + SetClientParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] + + # XXX TODO : stop reading from PLC .so file. This code is template code + # that can use modbus extension build data + for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters: + ParamFuncName = "__modbus_get_ServerNode_" + name + GetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + GetServerParamFuncs[name].restype = c_dtype + GetServerParamFuncs[name].argtypes = [ctypes.c_int] + + for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters: + ParamFuncName = "__modbus_set_ServerNode_" + name + SetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + SetServerParamFuncs[name].restype = None + SetServerParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] + + for node_id in range(client_count): + _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs) + + for node_id in range(server_count): + _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs) + + + + + +def _runtime_modbus_websettings_%(location_str)s_cleanup(): + """ + Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory + """ + + #PLCObject.LogMessage("Modbus web server extension::OnUnLoadPLC() Called...") + + # Delete the Modbus specific web interface extensions + # (Safe to ask to delete, even if it has not been added!) + global _WebNodeList + for index, WebNode_entry in enumerate(_WebNodeList): + config_hash = WebNode_entry["config_hash"] + NS.removeExtensionSetting(config_hash) + + # Dele all entries... + _WebNodeList = [] + diff -r 8b612b357679 -r 45f1ff6a7f87 runtime/NevowServer.py --- a/runtime/NevowServer.py Tue Jun 02 13:37:34 2020 +0200 +++ b/runtime/NevowServer.py Tue Jun 16 15:53:52 2020 +0200 @@ -26,6 +26,7 @@ from __future__ import absolute_import from __future__ import print_function import os +import collections import platform as platform_module from zope.interface import implements from nevow import appserver, inevow, tags, loaders, athena, url, rend @@ -180,10 +181,19 @@ setattr(self, 'action_' + name, callback) self.bindingsNames.append(name) - + ConfigurableSettings = ConfigurableBindings() +def newExtensionSetting(display, token): + global extensions_settings_od + settings = ConfigurableBindings() + extensions_settings_od[token] = (settings, display) + return settings + +def removeExtensionSetting(token): + global extensions_settings_od + extensions_settings_od.pop(token) class ISettings(annotate.TypedInterface): platform = annotate.String(label=_("Platform"), @@ -211,6 +221,7 @@ customSettingsURLs = { } +extensions_settings_od = collections.OrderedDict() class SettingsPage(rend.Page): # We deserve a slash @@ -221,6 +232,27 @@ child_webinterface_css = File(paths.AbsNeighbourFile(__file__, 'webinterface.css'), 'text/css') implements(ISettings) + + def __getattr__(self, name): + global extensions_settings_od + if name.startswith('configurable_'): + token = name[13:] + def configurable_something(ctx): + settings, _display = extensions_settings_od[token] + return settings + return configurable_something + raise AttributeError + + def extensions_settings(self, context, data): + """ Project extensions settings + Extensions added to Configuration Tree in IDE have their setting rendered here + """ + global extensions_settings_od + res = [] + for token in extensions_settings_od: + _settings, display = extensions_settings_od[token] + res += [tags.h2[display], webform.renderForms(token)] + return res docFactory = loaders.stan([tags.html[ tags.head[ @@ -238,12 +270,16 @@ webform.renderForms('staticSettings'), tags.h1["Extensions settings:"], webform.renderForms('dynamicSettings'), + extensions_settings ]]]) def configurable_staticSettings(self, ctx): return configurable.TypedInterfaceConfigurable(self) def configurable_dynamicSettings(self, ctx): + """ Runtime Extensions settings + Extensions loaded through Beremiz_service -e or optional runtime features render setting forms here + """ return ConfigurableSettings def sendLogMessage(self, level, message, **kwargs): diff -r 8b612b357679 -r 45f1ff6a7f87 util/ProcessLogger.py --- a/util/ProcessLogger.py Tue Jun 02 13:37:34 2020 +0200 +++ b/util/ProcessLogger.py Tue Jun 16 15:53:52 2020 +0200 @@ -31,6 +31,7 @@ from threading import Timer, Lock, Thread, Semaphore import signal +_debug = os.path.exists("BEREMIZ_DEBUG") class outputThread(Thread): """ @@ -77,6 +78,7 @@ timeout=None, outlimit=None, errlimit=None, endlog=None, keyword=None, kill_it=False, cwd=None, encoding=None, output_encoding=None): + assert(logger) self.logger = logger if not isinstance(Command, list): self.Command_str = Command @@ -174,8 +176,9 @@ self.endlog() def log_the_end(self, ecode, pid): - self.logger.write(self.Command_str + "\n") - self.logger.write_warning(_("exited with status {a1} (pid {a2})\n").format(a1=str(ecode), a2=str(pid))) + if self.logger is not None: + self.logger.write(self.Command_str + "\n") + self.logger.write_warning(_("exited with status {a1} (pid {a2})\n").format(a1=str(ecode), a2=str(pid))) def finish(self, pid, ecode): # avoid running function before start is finished @@ -184,7 +187,7 @@ if self.timeout: self.timeout.cancel() self.exitcode = ecode - if self.exitcode != 0: + if _debug or self.exitcode != 0: self.log_the_end(ecode, pid) if self.finish_callback is not None: self.finish_callback(self, ecode, pid)