--- 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
+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.split(os.path.dirname(os.path.realpath(__file__)))[0] base_folder = os.path.join(base_folder, "..")
-BacnetPath = os.path.join(base_folder, "BACnet")
+BacnetPath = os.path.join(base_folder, "BACnet") BacnetLibraryPath = os.path.join(BacnetPath, "lib")
BacnetIncludePath = os.path.join(BacnetPath, "include")
BacnetIncludePortPath = os.path.join(BacnetPath, "ports")
@@ -50,6 +51,10 @@
BACNET_VENDOR_NAME = "Beremiz.org"
BACNET_DEVICE_MODEL_NAME = "Beremiz PLC"
+# Max String Size of BACnet Paramaters +BACNET_PARAM_STRING_SIZE = 64 @@ -97,6 +102,14 @@
+ # NOTE; Add the following code/declaration to the aboce XSD in order to activate the + # Override_Parameters_Saved_on_PLC flag (currenty not in use as it requires further + # analysis how the user would interpret this user interface option. + # <xsd:attribute name="Override_Parameters_Saved_on_PLC" + # type="xsd:boolean" use="optional" default="true"/> # 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() + 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) + 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/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 <string.h> /* required for memcpy() */
#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);
+ // 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
+ /* 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)); struct timespec cur_time;
@@ -311,9 +345,22 @@
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)
+ /* only do the request if: + * - this request was explictly asked to be executed by the client program + * - 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)) + //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);
@@ -357,36 +404,40 @@
- // 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);
- * 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.
- * 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 */
- timespec_add(next_cycle, period_sec, period_nsec);
+ /* 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 ); - clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL);
+ // 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)); @@ -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) + 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)); +/* 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 + * 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. + 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){
- 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) { + client_nodes[index].node_address.addr.tcp.host = client_nodes[index].str1; + client_nodes[index].node_address.addr.tcp.service = client_nodes[index].str2; + client_nodes[index].node_address.addr.rtu.device = client_nodes[index].str1; + 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) { + server_nodes[index].node_address.addr.tcp.host = server_nodes[index].str1; + server_nodes[index].node_address.addr.tcp.service = server_nodes[index].str2; + server_nodes[index].node_address.addr.rtu.device = server_nodes[index].str1; /* modbus library init */
/* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus
@@ -421,9 +539,14 @@
- /* 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);
@@ -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); + 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); + 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; + 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); + client_nodes[index].init_state = 4; // we have created the timer /* launch a thread to handle this client node */
@@ -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));
- 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); - 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 @@
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 + 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 + 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: + * - execute user programs + * - 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_() + 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++) {
- 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);
- 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); + if (client_nodes[index].init_state >= 4) { + // timer was created, so we try to destroy it! + close = timer_delete(client_nodes[index].timer_id); + fprintf(stderr, "Modbus plugin: Error destroying timer for modbus client node %%s\n", client_nodes[index].location); + 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)); + fprintf(stderr, "Modbus plugin: Error destroying condition variable for modbus client node %%s\n", client_nodes[index].location); + if (client_nodes[index].init_state >= 2) { + // mutex was created, so we try to destroy it! + close = pthread_mutex_destroy(&(client_nodes[index].mutex)); + fprintf(stderr, "Modbus plugin: Error destroying mutex for modbus client node %%s\n", client_nodes[index].location); @@ -612,3 +838,122 @@
+/**********************************************/ +/** 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[] = { +#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 +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
@@ -39,8 +43,48 @@
u16 rw_words[MEM_AREA_SIZE];
+ * 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, + * addr.tcp.host and addr.tcp.service + * will point to str1 and str2 respectively + const char *config_name; + char str1[MODBUS_PARAM_STRING_SIZE]; + char str2[MODBUS_PARAM_STRING_SIZE]; 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
+ const char *config_name; + char str1[MODBUS_PARAM_STRING_SIZE]; + char str2[MODBUS_PARAM_STRING_SIZE]; node_addr_t node_address;
+ 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;// 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! + * 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) @@ -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 + /* 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 + /* 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 --- 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 */}'''
+{"%(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()))
- host, port, slaveid = GetCTVals(child, range(3))
+ config_name, host, port, slaveid = GetCTVals(child, range(4))
- 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)
node_dict = {"locnodestr": location,
+ "config_name": config_name, @@ -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)
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)
@@ -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 */}'''
+{"%(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()))
- device, baud, parity, stopbits, slaveid = GetCTVals(child, range(5))
+ config_name, device, baud, parity, stopbits, slaveid = GetCTVals(child, range(6)) node_dict = {"locnodestr": location,
+ "config_name": config_name, "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 */}'''
+{"%(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()))
- device, baud, parity, stopbits, coms_period = GetCTVals(child, range(5))
+ config_name, device, baud, parity, stopbits, coms_period = GetCTVals(child, range(6)) node_dict = {"locnodestr": location,
+ "config_name": config_name, "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 */}'''
+{"%(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()))
- host, port, coms_period = GetCTVals(child, range(3))
+ config_name, host, port, coms_period = GetCTVals(child, range(4)) node_dict = {"locnodestr": location,
+ "config_name": config_name, "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_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)
+ 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 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:attribute name="Write_on_change" type="xsd:boolean" use="optional" default="false"/> @@ -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 + # then the execution control flag will be + # and all the Modbus registers/coils will be + "name": "Exec. request flag", + "type": LOCATION_VAR_MEMORY, + "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", for offset in range(address, address + count):
"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: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:restriction base="xsd:unsignedLong">
- <xsd:minInclusive value="1"/>
+ <xsd:minInclusive value="0"/> <xsd:maxInclusive value="2147483647"/>
@@ -285,11 +312,33 @@
# TODO: Replace with CTNType !!!
PlugType = "ModbusTCPclient"
+ # 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 GetConfigName(self): + """ Return the node's Configuration_Name """ + return self.ModbusTCPclient.getConfiguration_Name() def CTNGenerate_C(self, buildpath, locations):
@@ -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: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"
+ # 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)
- # 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: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:restriction base="xsd:integer">
- <xsd:minInclusive value="1"/>
+ <xsd:minInclusive value="0"/> <xsd:maxInclusive value="2147483647"/>
@@ -403,6 +482,23 @@
# TODO: Replace with CTNType !!!
PlugType = "ModbusRTUclient"
+ # 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)
@@ -421,6 +517,10 @@
+ def GetConfigName(self): + """ Return the node's Configuration_Name """ + return self.ModbusRTUclient.getConfiguration_Name() def CTNGenerate_C(self, buildpath, locations):
@@ -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: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"
+ # 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)
@@ -489,6 +608,10 @@
+ def GetConfigName(self): + """ Return the node's Configuration_Name """ + return self.ModbusRTUslave.getConfiguration_Name() def CTNGenerate_C(self, buildpath, locations):
@@ -550,8 +673,7 @@
x1 + x2 for x1, x2 in zip(total_node_count, child.GetNodeCount()))
- # 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):
@@ -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
+ # 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
- 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").
- a1=_lt_to_str(IPServer_port_numbers[i][0]),
- a2=_lt_to_str(IPServer_port_numbers[j][0]),
- a3=IPServer_port_numbers[j][1]))
- # TODO: return an error code instead of raising an
+ # Note: We only consider duplicate port numbers if using the same network interface! + for loc1, addr1, port1 in IPServer_port_numbers[:-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
@@ -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 + # (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:
"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: + "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].flag_exec_req;" % (client_requestid)) + loc_vars_list.append(str(iecvar["NAME"])) tcpclient_node_count += 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 + # (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:
"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: + "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].flag_exec_req;" % (client_requestid)) + loc_vars_list.append(str(iecvar["NAME"])) rtuclient_node_count += 1
@@ -803,4 +993,18 @@
# LDFLAGS.append(" -lws2_32 ") # on windows we need to load winsock
- return [(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True
+ websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r') + websettingcode = websettingfile.read() + 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) + return ([(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True, + ("runtime_modbus_websettings_%s.py" % location_str, open(runtimefile_path, "rb")), --- 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 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) +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 @@
+extensions_settings_od = collections.OrderedDict() class SettingsPage(rend.Page):
@@ -221,6 +232,27 @@
child_webinterface_css = File(paths.AbsNeighbourFile(__file__, 'webinterface.css'), 'text/css')
+ def __getattr__(self, name): + global extensions_settings_od + if name.startswith('configurable_'): + def configurable_something(ctx): + settings, _display = extensions_settings_od[token] + return configurable_something + 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 + for token in extensions_settings_od: + _settings, display = extensions_settings_od[token] + res += [tags.h2[display], webform.renderForms(token)] docFactory = loaders.stan([tags.html[
@@ -238,12 +270,16 @@
webform.renderForms('staticSettings'),
tags.h1["Extensions settings:"],
webform.renderForms('dynamicSettings'),
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"> <pageSize x="1024" y="1024"/>
@@ -246,6 +246,25 @@
+ <variable name="Grumpf"> + <variable name="SomeVarName"> @@ -255,7 +274,7 @@
<relPosition x="160" y="15"/>
- <expression>'time.sleep(1)'</expression>
+ <expression>'666'</expression> <block localId="5" width="125" height="80" typeName="python_eval" instanceName="py1" executionOrderId="0">
<position x="686" y="400"/>
@@ -1118,23 +1137,12 @@
<expression>Second_Python_Var</expression>
- <outVariable localId="47" height="30" width="130" executionOrderId="0" negated="false">
- <position x="200" y="1385"/>
- <relPosition x="0" y="15"/>
- <connection refLocalId="59">
- <position x="200" y="1400"/>
- <position x="130" y="1400"/>
- <expression>Test_Python_Var</expression>
<inVariable localId="59" height="30" width="30" executionOrderId="0" negated="false">
<position x="100" y="1385"/>
<relPosition x="30" y="15"/>
- <expression>23</expression>
+ <expression>1</expression> <block localId="61" typeName="function0" executionOrderId="0" height="45" width="111">
<position x="760" y="1170"/>
@@ -1300,6 +1308,162 @@
+ <outVariable localId="72" executionOrderId="0" height="30" width="60" negated="false"> + <position x="1065" y="1970"/> + <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"/> + <expression>Grumpf</expression> + <inVariable localId="73" executionOrderId="0" height="30" width="85" negated="false"> + <position x="625" y="1940"/> + <relPosition x="85" y="15"/> + <expression>BOOL#TRUE</expression> + <inVariable localId="74" executionOrderId="0" height="30" width="70" negated="false"> + <position x="625" y="1975"/> + <relPosition x="70" y="15"/> + <expression>Test_DT</expression> + <block localId="75" typeName="RTC" instanceName="RTC0" executionOrderId="0" height="90" width="65"> + <position x="760" y="1925"/> + <variable formalParameter="IN"> + <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"/> + <variable formalParameter="PDT"> + <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"/> + <variable formalParameter="Q"> + <relPosition x="65" y="35"/> + <variable formalParameter="CDT"> + <relPosition x="65" y="70"/> + <block localId="76" typeName="DT_TO_STRING" executionOrderId="0" height="40" width="110"> + <position x="875" y="1965"/> + <variable formalParameter="IN"> + <relPosition x="0" y="30"/> + <connection refLocalId="75" formalParameter="CDT"> + <position x="875" y="1995"/> + <position x="825" y="1995"/> + <variable formalParameter="OUT"> + <relPosition x="110" y="30"/> + <block localId="77" typeName="ADD" executionOrderId="0" height="60" width="65"> + <position x="170" y="1370"/> + <variable formalParameter="IN1"> + <relPosition x="0" y="30"/> + <connection refLocalId="59"> + <position x="170" y="1400"/> + <position x="130" y="1400"/> + <variable formalParameter="IN2"> + <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"/> + <variable formalParameter="OUT"> + <relPosition x="65" y="30"/> + <outVariable localId="47" executionOrderId="0" height="30" width="130" negated="false"> + <position x="625" y="1335"/> + <relPosition x="0" y="15"/> + <connection refLocalId="79"> + <position x="625" y="1350"/> + <position x="590" y="1350"/> + <expression>Test_Python_Var</expression> + <inVariable localId="79" executionOrderId="0" height="25" width="30" negated="false"> + <position x="560" y="1340"/> + <relPosition x="30" y="10"/> + <expression>23</expression> + <inOutVariable localId="78" executionOrderId="0" height="30" width="100" negatedOut="false" negatedIn="false"> + <position x="280" y="1385"/> + <relPosition x="0" y="15"/> + <connection refLocalId="77" formalParameter="OUT"> + <position x="280" y="1400"/> + <position x="235" y="1400"/> + <relPosition x="100" y="15"/> + <expression>SomeVarName</expression> @@ -1447,7 +1611,7 @@
<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"/> --- 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
+_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):
if not isinstance(Command, list):
self.Command_str = Command
@@ -174,8 +176,9 @@
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 _debug or self.exitcode != 0: self.log_the_end(ecode, pid)
if self.finish_callback is not None:
self.finish_callback(self, ecode, pid)