--- a/Beremiz_service.py Thu Jun 18 10:42:08 2020 +0200
+++ b/Beremiz_service.py Thu Jun 18 11:00:26 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
--- a/bacnet/bacnet.py Thu Jun 18 10:42:08 2020 +0200
+++ b/bacnet/bacnet.py Thu Jun 18 11:00:26 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 @@
</xsd:element>
</xsd:schema>
"""
+ # 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 --->
+ # <xsd:attribute name="Override_Parameters_Saved_on_PLC"
+ # type="xsd:boolean" use="optional" default="true"/>
+ # <--- 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)
--- a/bacnet/runtime/device.h Thu Jun 18 10:42:08 2020 +0200
+++ b/bacnet/runtime/device.h Thu Jun 18 11:00:26 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 {
--- a/bacnet/runtime/server.c Thu Jun 18 10:42:08 2020 +0200
+++ b/bacnet/runtime/server.c Thu Jun 18 11:00:26 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;
+}
+
--- a/bacnet/runtime/server.h Thu Jun 18 10:42:08 2020 +0200
+++ b/bacnet/runtime/server.h Thu Jun 18 11:00:26 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_ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bacnet/web_settings.py Thu Jun 18 11:00:26 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
+
+
+
+
--- a/modbus/mb_runtime.c Thu Jun 18 10:42:08 2020 +0200
+++ b/modbus/mb_runtime.c Thu Jun 18 11:00:26 2020 +0200
@@ -25,6 +25,8 @@
#include <stdio.h>
#include <string.h> /* required for memcpy() */
+#include <time.h>
+#include <signal.h>
#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 <delay> until time to start next periodic/cyclic scan cycle
+ *
+ * In an attempt to be able to run the MB transactions during the <delay>
+ * 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;}
+
--- a/modbus/mb_runtime.h Thu Jun 18 10:42:08 2020 +0200
+++ b/modbus/mb_runtime.h Thu Jun 18 11:00:26 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;
--- a/modbus/mb_utils.py Thu Jun 18 10:42:08 2020 +0200
+++ b/modbus/mb_utils.py Thu Jun 18 11:00:26 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
--- a/modbus/modbus.py Thu Jun 18 10:42:08 2020 +0200
+++ b/modbus/modbus.py Thu Jun 18 11:00:26 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 @@
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
+ <xsd:attribute name="Write_on_change" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
@@ -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 = """<?xml version="1.0" encoding="ISO-8859-1" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="ModbusTCPclient">
<xsd:complexType>
+ <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="Remote_IP_Address" type="xsd:string" use="optional" default="localhost"/>
<xsd:attribute name="Remote_Port_Number" type="xsd:string" use="optional" default="502"/>
<xsd:attribute name="Invocation_Rate_in_ms" use="optional" default="100">
<xsd:simpleType>
<xsd:restriction base="xsd:unsignedLong">
- <xsd:minInclusive value="1"/>
+ <xsd:minInclusive value="0"/>
<xsd:maxInclusive value="2147483647"/>
</xsd:restriction>
</xsd:simpleType>
@@ -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 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="ModbusServerNode">
<xsd:complexType>
+ <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="Local_IP_Address" type="xsd:string" use="optional" default="#ANY#"/>
<xsd:attribute name="Local_Port_Number" type="xsd:string" use="optional" default="502"/>
<xsd:attribute name="SlaveID" use="optional" default="0">
@@ -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 = """<?xml version="1.0" encoding="ISO-8859-1" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="ModbusRTUclient">
<xsd:complexType>
+ <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="Serial_Port" type="xsd:string" use="optional" default="/dev/ttyS0"/>
<xsd:attribute name="Baud_Rate" type="xsd:string" use="optional" default="9600"/>
<xsd:attribute name="Parity" type="xsd:string" use="optional" default="even"/>
@@ -388,7 +467,7 @@
<xsd:attribute name="Invocation_Rate_in_ms" use="optional" default="100">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
- <xsd:minInclusive value="1"/>
+ <xsd:minInclusive value="0"/>
<xsd:maxInclusive value="2147483647"/>
</xsd:restriction>
</xsd:simpleType>
@@ -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 = """<?xml version="1.0" encoding="ISO-8859-1" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="ModbusRTUslave">
<xsd:complexType>
+ <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="Serial_Port" type="xsd:string" use="optional" default="/dev/ttyS0"/>
<xsd:attribute name="Baud_Rate" type="xsd:string" use="optional" default="9600"/>
<xsd:attribute name="Parity" type="xsd:string" use="optional" default="even"/>
@@ -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")),
+ )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modbus/web_settings.py Thu Jun 18 11:00:26 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 = []
+
--- a/py_ext/PythonFileCTNMixin.py Thu Jun 18 10:42:08 2020 +0200
+++ b/py_ext/PythonFileCTNMixin.py Thu Jun 18 11:00:26 2020 +0200
@@ -242,8 +242,8 @@
varpubonchangefmt = """\
if(!AtomicCompareExchange(&__%(name)s_rlock, 0, 1)){
IEC_%(IECtype)s tmp = __GET_VAR(%(configname)s__%(uppername)s);
- if(__%(name)s_rbuffer != tmp){
- __%(name)s_rbuffer = %(configname)s__%(uppername)s.value;
+ if(NE_%(IECtype)s(1, NULL, __%(name)s_rbuffer, tmp)){
+ __%(name)s_rbuffer = tmp;
PYTHON_POLL_body__(__%(name)s_notifier);
}
AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0);
--- a/runtime/NevowServer.py Thu Jun 18 10:42:08 2020 +0200
+++ b/runtime/NevowServer.py Thu Jun 18 11:00:26 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):
--- a/tests/python/plc.xml Thu Jun 18 10:42:08 2020 +0200
+++ b/tests/python/plc.xml Thu Jun 18 11:00:26 2020 +0200
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://www.plcopen.org/xml/tc6_0201" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xhtml="http://www.w3.org/1999/xhtml" xsi:schemaLocation="http://www.plcopen.org/xml/tc6_0201">
<fileHeader companyName="" productName="Beremiz" productVersion="0.0" creationDateTime="2008-12-14T16:21:19" contentDescription="This example shows many features in Beremiz: 1. How to implement python extensions. 2. How to implement basic C extension. 3. How to use C code in IEC POUs. 4. How to call C functions from python code. 5. How to avoid race conditions between IEC, C and python code. 6. How to convert betweet different IEC types. "/>
- <contentHeader name="Beremiz Python Support Tests" modificationDateTime="2019-09-24T11:49:14">
+ <contentHeader name="Beremiz Python Support Tests" modificationDateTime="2020-06-17T13:19:14">
<coordinateInfo>
<pageSize x="1024" y="1024"/>
<fbd>
@@ -246,6 +246,25 @@
<INT/>
</type>
</variable>
+ <variable name="Grumpf">
+ <type>
+ <string/>
+ </type>
+ </variable>
+ </externalVars>
+ <localVars>
+ <variable name="RTC0">
+ <type>
+ <derived name="RTC"/>
+ </type>
+ </variable>
+ </localVars>
+ <externalVars>
+ <variable name="SomeVarName">
+ <type>
+ <DINT/>
+ </type>
+ </variable>
</externalVars>
</interface>
<body>
@@ -255,7 +274,7 @@
<connectionPointOut>
<relPosition x="160" y="15"/>
</connectionPointOut>
- <expression>'time.sleep(1)'</expression>
+ <expression>'666'</expression>
</inVariable>
<block localId="5" width="125" height="80" typeName="python_eval" instanceName="py1" executionOrderId="0">
<position x="686" y="400"/>
@@ -1118,23 +1137,12 @@
</connectionPointOut>
<expression>Second_Python_Var</expression>
</inVariable>
- <outVariable localId="47" height="30" width="130" executionOrderId="0" negated="false">
- <position x="200" y="1385"/>
- <connectionPointIn>
- <relPosition x="0" y="15"/>
- <connection refLocalId="59">
- <position x="200" y="1400"/>
- <position x="130" y="1400"/>
- </connection>
- </connectionPointIn>
- <expression>Test_Python_Var</expression>
- </outVariable>
<inVariable localId="59" height="30" width="30" executionOrderId="0" negated="false">
<position x="100" y="1385"/>
<connectionPointOut>
<relPosition x="30" y="15"/>
</connectionPointOut>
- <expression>23</expression>
+ <expression>1</expression>
</inVariable>
<block localId="61" typeName="function0" executionOrderId="0" height="45" width="111">
<position x="760" y="1170"/>
@@ -1300,6 +1308,162 @@
]]></xhtml:p>
</content>
</comment>
+ <outVariable localId="72" executionOrderId="0" height="30" width="60" negated="false">
+ <position x="1065" y="1970"/>
+ <connectionPointIn>
+ <relPosition x="0" y="15"/>
+ <connection refLocalId="76" formalParameter="OUT">
+ <position x="1065" y="1985"/>
+ <position x="1025" y="1985"/>
+ <position x="1025" y="1995"/>
+ <position x="985" y="1995"/>
+ </connection>
+ </connectionPointIn>
+ <expression>Grumpf</expression>
+ </outVariable>
+ <inVariable localId="73" executionOrderId="0" height="30" width="85" negated="false">
+ <position x="625" y="1940"/>
+ <connectionPointOut>
+ <relPosition x="85" y="15"/>
+ </connectionPointOut>
+ <expression>BOOL#TRUE</expression>
+ </inVariable>
+ <inVariable localId="74" executionOrderId="0" height="30" width="70" negated="false">
+ <position x="625" y="1975"/>
+ <connectionPointOut>
+ <relPosition x="70" y="15"/>
+ </connectionPointOut>
+ <expression>Test_DT</expression>
+ </inVariable>
+ <block localId="75" typeName="RTC" instanceName="RTC0" executionOrderId="0" height="90" width="65">
+ <position x="760" y="1925"/>
+ <inputVariables>
+ <variable formalParameter="IN">
+ <connectionPointIn>
+ <relPosition x="0" y="35"/>
+ <connection refLocalId="73">
+ <position x="760" y="1960"/>
+ <position x="735" y="1960"/>
+ <position x="735" y="1955"/>
+ <position x="710" y="1955"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="PDT">
+ <connectionPointIn>
+ <relPosition x="0" y="70"/>
+ <connection refLocalId="74">
+ <position x="760" y="1995"/>
+ <position x="727" y="1995"/>
+ <position x="727" y="1990"/>
+ <position x="695" y="1990"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="Q">
+ <connectionPointOut>
+ <relPosition x="65" y="35"/>
+ </connectionPointOut>
+ </variable>
+ <variable formalParameter="CDT">
+ <connectionPointOut>
+ <relPosition x="65" y="70"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <block localId="76" typeName="DT_TO_STRING" executionOrderId="0" height="40" width="110">
+ <position x="875" y="1965"/>
+ <inputVariables>
+ <variable formalParameter="IN">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="75" formalParameter="CDT">
+ <position x="875" y="1995"/>
+ <position x="825" y="1995"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="110" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <block localId="77" typeName="ADD" executionOrderId="0" height="60" width="65">
+ <position x="170" y="1370"/>
+ <inputVariables>
+ <variable formalParameter="IN1">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="59">
+ <position x="170" y="1400"/>
+ <position x="130" y="1400"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN2">
+ <connectionPointIn>
+ <relPosition x="0" y="50"/>
+ <connection refLocalId="78">
+ <position x="170" y="1420"/>
+ <position x="160" y="1420"/>
+ <position x="160" y="1450"/>
+ <position x="390" y="1450"/>
+ <position x="390" y="1400"/>
+ <position x="380" y="1400"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="65" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <outVariable localId="47" executionOrderId="0" height="30" width="130" negated="false">
+ <position x="625" y="1335"/>
+ <connectionPointIn>
+ <relPosition x="0" y="15"/>
+ <connection refLocalId="79">
+ <position x="625" y="1350"/>
+ <position x="590" y="1350"/>
+ </connection>
+ </connectionPointIn>
+ <expression>Test_Python_Var</expression>
+ </outVariable>
+ <inVariable localId="79" executionOrderId="0" height="25" width="30" negated="false">
+ <position x="560" y="1340"/>
+ <connectionPointOut>
+ <relPosition x="30" y="10"/>
+ </connectionPointOut>
+ <expression>23</expression>
+ </inVariable>
+ <inOutVariable localId="78" executionOrderId="0" height="30" width="100" negatedOut="false" negatedIn="false">
+ <position x="280" y="1385"/>
+ <connectionPointIn>
+ <relPosition x="0" y="15"/>
+ <connection refLocalId="77" formalParameter="OUT">
+ <position x="280" y="1400"/>
+ <position x="235" y="1400"/>
+ </connection>
+ </connectionPointIn>
+ <connectionPointOut>
+ <relPosition x="100" y="15"/>
+ </connectionPointOut>
+ <expression>SomeVarName</expression>
+ </inOutVariable>
</FBD>
</body>
</pou>
@@ -1447,7 +1611,7 @@
<configurations>
<configuration name="config">
<resource name="res_pytest">
- <task name="pytest_task" interval="T#1ms" priority="0"/>
+ <task name="pytest_task" priority="0" interval="T#500ms"/>
<globalVars>
<variable name="TOTO">
<type>
--- a/tests/python/py_ext_0@py_ext/pyfile.xml Thu Jun 18 10:42:08 2020 +0200
+++ b/tests/python/py_ext_0@py_ext/pyfile.xml Thu Jun 18 11:00:26 2020 +0200
@@ -1,13 +1,17 @@
<?xml version='1.0' encoding='utf-8'?>
<PyFile xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<variables>
- <variable name="SomeVarName" type="DINT"/>
- <variable name="Grumpf" type="STRING"/>
+ <variable name="SomeVarName" type="DINT" onchange="MyFunc"/>
+ <variable name="Grumpf" type="STRING" initial="'mhoo'" onchange="MyFunc"/>
</variables>
<globals>
<xhtml:p><![CDATA[
print "All python PLC globals variables :", PLCGlobalsDesc
print "Current extention name :", __ext_name__
+
+def MyFunc(*args):
+ print args
+
]]></xhtml:p>
</globals>
<init>
--- a/tests/python/python@py_ext/pyfile.xml Thu Jun 18 10:42:08 2020 +0200
+++ b/tests/python/python@py_ext/pyfile.xml Thu Jun 18 11:00:26 2020 +0200
@@ -20,6 +20,7 @@
print "Failed Python_to_C_Call failed"
res = None
print "Python read PLC global :",PLCGlobals.Test_Python_Var
+ print "Python read PLC global Grumpf :",PLCGlobals.Grumpf
PLCGlobals.Second_Python_Var = 789
sys.stdout.flush()
return res
--- a/util/ProcessLogger.py Thu Jun 18 10:42:08 2020 +0200
+++ b/util/ProcessLogger.py Thu Jun 18 11:00:26 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)