# HG changeset patch # User Mario de Sousa # Date 1590661019 -3600 # Node ID 449c9539887a9df270ee962816646778be4230a0 # Parent 769fefae7c81385ad49c4745506428f445004c25# Parent db68cb0e6bdcdcb10272f5ef1f8a4b9377d81fa8 merge diff -r 769fefae7c81 -r 449c9539887a Beremiz_service.py --- a/Beremiz_service.py Wed Dec 18 13:31:22 2019 +0100 +++ b/Beremiz_service.py Thu May 28 11:16:59 2020 +0100 @@ -492,6 +492,8 @@ installThreadExcepthook() havewamp = False +haveBNconf = False + if havetwisted: if webport is not None: @@ -500,8 +502,17 @@ except Exception: LogMessageAndException(_("Nevow/Athena import failed :")) webport = None - NS.WorkingDir = WorkingDir - + NS.WorkingDir = WorkingDir # bug? what happens if import fails? + + # Try to add support for BACnet configuration via web server interface + # NOTE:BACnet web config only makes sense if web server is available + if webport is not None: + try: + import runtime.BACnet_config as BNconf + haveBNconf = True + except Exception: + LogMessageAndException(_("BACnet configuration web interface - import failed :")) + try: import runtime.WampClient as WC # pylint: disable=ungrouped-imports WC.WorkingDir = WorkingDir @@ -523,6 +534,8 @@ runtime.CreatePLCObjectSingleton( WorkingDir, argv, statuschange, evaluator, pyruntimevars) +plcobj = runtime.GetPLCObjectSingleton() + pyroserver = PyroServer(servicename, interface, port) if havewx: @@ -538,6 +551,12 @@ except Exception: LogMessageAndException(_("Nevow Web service failed. ")) + if haveBNconf: + try: + BNconf.init(plcobj, NS, WorkingDir) + except Exception: + LogMessageAndException(_("BACnet web configuration failed startup. ")) + if havewamp: try: WC.SetServer(pyroserver) @@ -601,7 +620,6 @@ pyroserver.Quit() pyro_thread.join() -plcobj = runtime.GetPLCObjectSingleton() plcobj.StopPLC() plcobj.UnLoadPLC() diff -r 769fefae7c81 -r 449c9539887a bacnet/bacnet.py --- a/bacnet/bacnet.py Wed Dec 18 13:31:22 2019 +0100 +++ b/bacnet/bacnet.py Thu May 28 11:16:59 2020 +0100 @@ -35,11 +35,11 @@ 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 + +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 +50,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 +101,14 @@ """ + # NOTE; Add the following code/declaration to the aboce XSD in order to activate the + # Override_Parameters_Saved_on_PLC flag (currenty not in use as it requires further + # analysis how the user would interpret this user interface option. + # <--- snip ---> + # + # <--- snip ---> + # # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID) # so the Device instance ID is limited from 0 to 22^2-1 = 4194303 # However, 4194303 is reserved for special use (similar to NULL pointer), so last @@ -552,8 +564,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 +610,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 +626,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 +747,33 @@ CFLAGS = ' -I"' + BacnetIncludePath + '"' CFLAGS += ' -I"' + BacnetIncludePortPath + '"' + # ---------------------------------------------------------------------- + # 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! return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True + #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle) diff -r 769fefae7c81 -r 449c9539887a bacnet/runtime/device.h --- a/bacnet/runtime/device.h Wed Dec 18 13:31:22 2019 +0100 +++ b/bacnet/runtime/device.h Thu May 28 11:16:59 2020 +0100 @@ -161,11 +161,12 @@ * Maximum sizes excluding nul terminator . */ #define STRLEN_X(minlen, str) (((minlen)>sizeof(str))?(minlen):sizeof(str)) -#define MAX_DEV_NAME_LEN STRLEN_X(32, "%(BACnet_Device_Name)s") /* Device name */ -#define MAX_DEV_LOC_LEN STRLEN_X(64, BACNET_DEVICE_LOCATION) /* Device location */ -#define MAX_DEV_MOD_LEN STRLEN_X(32, BACNET_DEVICE_MODEL_NAME) /* Device model name */ -#define MAX_DEV_VER_LEN STRLEN_X(16, BACNET_DEVICE_APPSOFT_VER) /* Device application software version */ -#define MAX_DEV_DESC_LEN STRLEN_X(64, BACNET_DEVICE_DESCRIPTION) /* Device description */ +#define MAX_DEV_NAME_LEN STRLEN_X(%(BACnet_Param_String_Size)d, "%(BACnet_Device_Name)s") /* Device name */ +#define MAX_DEV_LOC_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_LOCATION) /* Device location */ +#define MAX_DEV_MOD_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_MODEL_NAME) /* Device model name */ +#define MAX_DEV_VER_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_APPSOFT_VER) /* Device application software version */ +#define MAX_DEV_DESC_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_DESCRIPTION) /* Device description */ + /** Structure to define the Object Properties common to all Objects. */ typedef struct commonBacObj_s { diff -r 769fefae7c81 -r 449c9539887a bacnet/runtime/server.c --- a/bacnet/runtime/server.c Wed Dec 18 13:31:22 2019 +0100 +++ b/bacnet/runtime/server.c Thu May 28 11:16:59 2020 +0100 @@ -52,6 +52,10 @@ #include "timesync.h" + + + + /* A utility function used by most (all?) implementations of BACnet Objects */ /* Adds to Prop_List all entries in Prop_List_XX that are not * PROP_OBJECT_IDENTIFIER, PROP_OBJECT_NAME, PROP_OBJECT_TYPE, PROP_PROPERTY_LIST @@ -454,17 +458,33 @@ bvlc_bdt_restore_local(); /* Initiliaze the bacnet server 'device' */ Device_Init(server_node->device_name); - - pthread_mutex_lock(&init_done_lock); - init_done = 1; - pthread_cond_signal(&init_done_cond); - pthread_mutex_unlock(&init_done_lock); - + + /* Although the default values for the following properties are hardcoded into + * the respective variable definition+initialization in the C code, + * these values may be potentially changed after compilation but before + * code startup. This is done by the web interface(1), directly loading the .so file, + * and changing the values in the server_node_t variable. + * We must therefore honor those values when we start running the BACnet device server + * + * (1) Web interface is implemented in runtime/BACnet_config.py + * which works as an extension of the web server in runtime/NevowServer.py + * which in turn is initialised/run by the Beremiz_service.py deamon + */ + Device_Set_Location (server_node->device_location, strlen(server_node->device_location)); + Device_Set_Description (server_node->device_description, strlen(server_node->device_description)); + Device_Set_Application_Software_Version(server_node->device_appsoftware_ver, strlen(server_node->device_appsoftware_ver)); /* Set the password (max 31 chars) for Device Communication Control request. */ /* Default in the BACnet stack is hardcoded as "filister" */ /* (char *) cast is to remove the cast. The function is incorrectly declared/defined in the BACnet stack! */ /* BACnet stack needs to change demo/handler/h_dcc.c and include/handlers.h */ handler_dcc_password_set((char *)server_node->comm_control_passwd); + + + pthread_mutex_lock(&init_done_lock); + init_done = 1; + pthread_cond_signal(&init_done_cond); + pthread_mutex_unlock(&init_done_lock); + /* Set callbacks and configure network interface */ res = Init_Service_Handlers(); if (res < 0) exit(1); @@ -661,3 +681,108 @@ return res; } + + + + +/**********************************************/ +/** Functions for Beremiz web interface. **/ +/**********************************************/ + +/* + * Beremiz has a program to run on the PLC (Beremiz_service.py) + * to handle downloading of compiled programs, start/stop of PLC, etc. + * (see runtime/PLCObject.py for start/stop, loading, ...) + * + * This service also includes a web server to access PLC state (start/stop) + * and to change some basic confiuration parameters. + * (see runtime/NevowServer.py for the web server) + * + * The web server allows for extensions, where additional configuration + * parameters may be changed on the running/downloaded PLC. + * BACnet plugin also comes with an extension to the web server, through + * which the basic BACnet plugin configuration parameters may be changed + * (basically, only the parameters in server_node_t may be changed) + * + * The following functions are never called from other C code. They are + * called instead from the python code in runtime/BACnet_config.py, that + * implements the web server extension for configuring BACnet parameters. + */ + + +/* The location (in the Config. Node Tree of Beremiz IDE) + * of the BACnet plugin. + * + * This variable is also used by the BACnet web config code to determine + * whether the current loaded PLC includes the BACnet plugin + * (so it should make the BACnet parameter web interface visible to the user). + */ +const char * __bacnet_plugin_location = "%(locstr)s"; + + +/* NOTE: We could have the python code in runtime/BACnet_config.py + * directly access the server_node_t structure, however + * this would create a tight coupling between these two + * disjoint pieces of code. + * Any change to the server_node_t structure would require the + * python code to be changed accordingly. I have therefore opted + * to cretae get/set functions, one for each parameter. + * + * NOTE: since the BACnet plugin can only support/allow at most one instance + * of the BACnet plugin in Beremiz (2 or more are not allowed due to + * limitations of the underlying BACnet protocol stack being used), + * a single generic version of each of the following functions would work. + * However, simply for the sake of keeping things general, we create + * a diferent function for each plugin instance (which, as I said, + * will never occur for now). + * + * The functions being declared are therefoe named: + * __bacnet_0_get_ConfigParam_location + * __bacnet_0_get_ConfigParam_device_name + * etc... + * where the 0 will be replaced by the location of the BACnet plugin + * in the Beremiz configuration tree (will change depending on where + * the user inserted the BACnet plugin into their project) + */ + +/* macro works for all data types */ +#define __bacnet_get_ConfigParam(param_type,param_name) \ +param_type __bacnet_%(locstr)s_get_ConfigParam_##param_name(void) { \ + return server_node.param_name; \ +} + +/* macro only works for char * data types */ +/* Note that the storage space (max string size) reserved for each parameter + * (this storage space is reserved in device.h) + * is set to a minimum of + * %(BACnet_Param_String_Size)d + * which is set to the value of + * BACNET_PARAM_STRING_SIZE + * in bacnet.py + */ +#define __bacnet_set_ConfigParam(param_type,param_name) \ +void __bacnet_%(locstr)s_set_ConfigParam_##param_name(param_type val) { \ + strncpy(server_node.param_name, val, %(BACnet_Param_String_Size)d); \ + server_node.param_name[%(BACnet_Param_String_Size)d - 1] = '\0'; \ +} + + +#define __bacnet_ConfigParam_str(param_name) \ +__bacnet_get_ConfigParam(const char*,param_name) \ +__bacnet_set_ConfigParam(const char*,param_name) + + +__bacnet_ConfigParam_str(location) +__bacnet_ConfigParam_str(network_interface) +__bacnet_ConfigParam_str(port_number) +__bacnet_ConfigParam_str(device_name) +__bacnet_ConfigParam_str(device_location) +__bacnet_ConfigParam_str(device_description) +__bacnet_ConfigParam_str(device_appsoftware_ver) +__bacnet_ConfigParam_str(comm_control_passwd) + +__bacnet_get_ConfigParam(uint32_t,device_id) +void __bacnet_%(locstr)s_set_ConfigParam_device_id(uint32_t val) { + server_node.device_id = val; +} + diff -r 769fefae7c81 -r 449c9539887a bacnet/runtime/server.h --- a/bacnet/runtime/server.h Wed Dec 18 13:31:22 2019 +0100 +++ b/bacnet/runtime/server.h Thu May 28 11:16:59 2020 +0100 @@ -33,12 +33,19 @@ + typedef struct{ - const char *location; - const char *network_interface; - const char *port_number; - const char *device_name; - const char *comm_control_passwd; + char location [%(BACnet_Param_String_Size)d]; + char network_interface [%(BACnet_Param_String_Size)d]; + char port_number [%(BACnet_Param_String_Size)d]; + char device_name [%(BACnet_Param_String_Size)d]; + char device_location [%(BACnet_Param_String_Size)d]; + char device_description [%(BACnet_Param_String_Size)d]; + char device_appsoftware_ver[%(BACnet_Param_String_Size)d]; + char comm_control_passwd [%(BACnet_Param_String_Size)d]; +// int override_local_config; // bool flag => +// // true : use these parameter values +// // false: use values stored on local file in PLC uint32_t device_id; // device ID is 22 bits long! uint16_t is not enough! int init_state; // store how far along the server's initialization has progressed pthread_t thread_id; // thread handling this server @@ -49,13 +56,16 @@ /*initialization following all parameters given by user in application*/ static server_node_t server_node = { "%(locstr)s", - "%(network_interface)s", // interface (NULL => use default (eth0)) - "%(port_number)s", // Port number (NULL => use default) - "%(BACnet_Device_Name)s", // BACnet server's device (object) name - "%(BACnet_Comm_Control_Password)s",// BACnet server's device (object) name - %(BACnet_Device_ID)s // BACnet server's device (object) ID + "%(network_interface)s", // interface (NULL => use default (eth0)) + "%(port_number)s", // Port number (NULL => use default) + "%(BACnet_Device_Name)s", // BACnet server's device (object) Name + "%(BACnet_Device_Location)s", // BACnet server's device (object) Location + "%(BACnet_Device_Description)s", // BACnet server's device (object) Description + "%(BACnet_Device_AppSoft_Version)s", // BACnet server's device (object) App. Software Ver. + "%(BACnet_Comm_Control_Password)s", // BACnet server's device (object) Password +// (Override_Parameters_Saved_on_PLC)d, // override locally saved parameters (bool flag) + %(BACnet_Device_ID)s // BACnet server's device (object) ID }; - #endif /* SERVER_H_ */ diff -r 769fefae7c81 -r 449c9539887a modbus/mb_runtime.c --- a/modbus/mb_runtime.c Wed Dec 18 13:31:22 2019 +0100 +++ b/modbus/mb_runtime.c Thu May 28 11:16:59 2020 +0100 @@ -25,6 +25,8 @@ #include #include /* required for memcpy() */ +#include +#include #include "mb_slave_and_master.h" #include "MB_%(locstr)s.h" @@ -299,10 +301,42 @@ // Enable thread cancelation. Enabled is default, but set it anyway to be safe. pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - // get the current time - clock_gettime(CLOCK_MONOTONIC, &next_cycle); - - // loop the communication with the client + // configure the timer for periodic activation + { + struct itimerspec timerspec; + timerspec.it_interval.tv_sec = period_sec; + timerspec.it_interval.tv_nsec = period_nsec; + timerspec.it_value = timerspec.it_interval; + + if (timer_settime(client_nodes[client_node_id].timer_id, 0 /* flags */, &timerspec, NULL) < 0) + fprintf(stderr, "Modbus plugin: Error configuring periodic activation timer for Modbus client %%s.\n", client_nodes[client_node_id].location); + } + + /* loop the communication with the client + * + * When the client thread has difficulty communicating with remote client and/or server (network issues, for example), + * then the communications get delayed and we will fall behind in the period. + * + * This is OK. Note that if the condition variable were to be signaled multiple times while the client thread is inside the same + * Modbus transaction, then all those signals would be ignored. + * However, and since we keep the mutex locked during the communication cycle, it is not possible to signal the condition variable + * during that time (it is only possible while the thread is blocked during the call to pthread_cond_wait(). + * + * This means that when network issues eventually get resolved, we will NOT have a bunch of delayed activations to handle + * in quick succession (which would goble up CPU time). + * + * Notice that the above property is valid whether the communication cycle is run with the mutex locked, or unlocked. + * Since it makes it easier to implement the correct semantics for the other activation methods if the communication cycle + * is run with the mutex locked, then that is what we do. + * + * Note that during all the communication cycle we will keep locked the mutex + * (i.e. the mutex used together with the condition variable that will activate a new communication cycle) + * + * Note that we never get to explicitly unlock this mutex. It will only be unlocked by the pthread_cond_wait() + * call at the end of the cycle. + */ + pthread_mutex_lock(&(client_nodes[client_node_id].mutex)); + while (1) { /* struct timespec cur_time; @@ -311,9 +345,22 @@ */ int req; for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){ - /*just do the requests belonging to the client */ + /* just do the requests belonging to the client */ if (client_requests[req].client_node_id != client_node_id) continue; + + /* only do the request if: + * - this request was explictly asked to be executed by the client program + * OR + * - the client thread was activated periodically + * (in which case we execute all the requests belonging to the client node) + */ + if ((client_requests[req].flag_exec_req == 0) && (client_nodes[client_requests[req].client_node_id].periodic_act == 0)) + continue; + + //fprintf(stderr, "Modbus plugin: RUNNING<###> of Modbus request %%d (periodic = %%d flag_exec_req = %%d)\n", + // req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req ); + int res_tmp = __execute_mb_request(req); switch (res_tmp) { case PORT_FAILURE: { @@ -357,36 +404,40 @@ break; } } - } - // Determine absolute time instant for starting the next cycle - struct timespec prev_cycle, now; - prev_cycle = next_cycle; - timespec_add(next_cycle, period_sec, period_nsec); - /* NOTE A: - * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will - * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously - * for some time until we catch up. - * This is undesirable, so we detect it by making sure the next_cycle will start in the future. - * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval. - * - * NOTE B: - * It probably does not make sense to check for overflow of timer - so we don't do it for now! - * Even in 32 bit systems this will take at least 68 years since the computer booted - * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0 - * every time the system boots). On 64 bit systems, it will take over - * 10^11 years to overflow. - */ - clock_gettime(CLOCK_MONOTONIC, &now); - if ( ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec))) - /* We are falling behind. See NOTE A above */ - || (next_cycle.tv_sec < prev_cycle.tv_sec) - /* Timer overflow. See NOTE B above */ - ) { - next_cycle = now; - timespec_add(next_cycle, period_sec, period_nsec); - } - - clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL); + + /* We have just finished excuting a client transcation request. + * If the current cycle was activated by user request we reset the flag used to ask to run it + */ + if (0 != client_requests[req].flag_exec_req) { + client_requests[req].flag_exec_req = 0; + client_requests[req].flag_exec_started = 0; + } + + //fprintf(stderr, "Modbus plugin: RUNNING<---> of Modbus request %%d (periodic = %%d flag_exec_req = %%d)\n", + // req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req ); + } + + // Wait for signal (from timer or explicit request from user program) before starting the next cycle + { + // No need to lock the mutex. Is is already locked just before the while(1) loop. + // Read the comment there to understand why. + // pthread_mutex_lock(&(client_nodes[client_node_id].mutex)); + + /* the client thread has just finished a cycle, so all the flags used to signal an activation + * and specify the activation source (periodic, user request, ...) + * get reset here, before waiting for a new activation. + */ + client_nodes[client_node_id].periodic_act = 0; + client_nodes[client_node_id].execute_req = 0; + + while (client_nodes[client_node_id].execute_req == 0) + pthread_cond_wait(&(client_nodes[client_node_id].condv), + &(client_nodes[client_node_id].mutex)); + + // We run the communication cycle with the mutex locked. + // Read the comment just above the while(1) to understand why. + // pthread_mutex_unlock(&(client_nodes[client_node_id].mutex)); + } } // humour the compiler. @@ -394,6 +445,50 @@ } + +/* 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; @@ -421,9 +516,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 +543,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 +583,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 +632,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 +659,33 @@ pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex)); } } - } + /* if the user program set the execution request flag, then activate the thread + * that handles this Modbus client transaction so it gets a chance to be executed + * (but don't activate the thread if it has already been activated!) + * + * NOTE that we do this, for both the IN and OUT mapped location, under this + * __publish_() function. The scan cycle of the PLC works as follows: + * - call __retrieve_() + * - execute user programs + * - call __publish_() + * - insert until time to start next periodic/cyclic scan cycle + * + * In an attempt to be able to run the MB transactions during the + * interval in which not much is going on, we handle the user program + * requests to execute a specific MB transaction in this __publish_() + * function. + */ + if ((client_requests[index].flag_exec_req != 0) && (0 == client_requests[index].flag_exec_started)) { + int client_node_id = client_requests[index].client_node_id; + if (__signal_client_thread(client_node_id) >= 0) { + /* - upon success, set flag_exec_started + * - both flags (flag_exec_req and flag_exec_started) will be reset + * once the transaction has completed. + */ + client_requests[index].flag_exec_started = 1; + } + } + } } @@ -544,12 +720,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; diff -r 769fefae7c81 -r 449c9539887a modbus/mb_runtime.h --- a/modbus/mb_runtime.h Wed Dec 18 13:31:22 2019 +0100 +++ b/modbus/mb_runtime.h Thu May 28 11:16:59 2020 +0100 @@ -54,11 +54,32 @@ typedef struct{ const char *location; 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 +103,37 @@ u8 error_code; // modbus error code (if any) of current request int prev_error; // error code of the last printed error message (0 when no error) struct timespec resp_timeout; + u8 write_on_change; // boolean flag. If true => execute MB request when data to send changes // buffer used to store located PLC variables u16 plcv_buffer[REQ_BUF_SIZE]; // buffer used to store data coming from / going to server u16 coms_buffer[REQ_BUF_SIZE]; pthread_mutex_t coms_buf_mutex; // mutex to access coms_buffer[] + /* boolean flag that will be mapped onto a (BOOL) located variable + * (u16 because IEC 61131-3 BOOL are mapped onto u16 in C code! ) + * -> allow PLC program to request when to start the MB transaction + * -> will be reset once the MB transaction has completed + */ + u16 flag_exec_req; + /* flag that works in conjunction with flag_exec_req + * (does not really need to be u16 as it is not mapped onto a located variable. ) + * -> used by internal logic to indicate that the client thread + * that will be executing the MB transaction + * requested by flag exec_req has already been activated. + * -> will be reset once the MB transaction has completed + */ + u16 flag_exec_started; + /* flag that will be mapped onto a (WORD) located variable + * (u16 because the flag is a word! ) + * -> MSByte will store the result of the last executed MB transaction + * 1 -> error accessing IP network, or serial interface + * 2 -> reply received from server was an invalid frame + * 3 -> server did not reply before timeout expired + * 4 -> server returned a valid error frame + * -> if the MSByte is 4, the LSByte will store the MB error code returned by the server + * -> will be reset (set to 0) once this MB transaction has completed sucesfully + */ + u16 flag_exec_status; } client_request_t; diff -r 769fefae7c81 -r 449c9539887a modbus/mb_utils.py --- a/modbus/mb_utils.py Wed Dec 18 13:31:22 2019 +0100 +++ b/modbus/mb_utils.py Thu May 28 11:16:59 2020 +0100 @@ -184,7 +184,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 +198,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 +223,11 @@ self.GetCTRoot().logger.write_error( "Modbus plugin: Invalid number of channels in TCP client request node %(locreqstr)s (start_address + nr_channels must be less than 65536)\nModbus plugin: Aborting C code generation for this node\n" % request_dict) return None + if (request_dict["write_on_change"] and (request_dict["iotype"] == 'req_input')): + self.GetCTRoot().logger.write_error( + "Modbus plugin: (warning) MB client request node %(locreqstr)s has option 'write_on_change' enabled.\nModbus plugin: This option will be ignored by the Modbus read function.\n" % request_dict) + # NOTE: this is only a warning (we don't wish to abort code generation) so following line must be left commented out! + # return None + return req_init_template % request_dict diff -r 769fefae7c81 -r 449c9539887a modbus/modbus.py --- a/modbus/modbus.py Wed Dec 18 13:31:22 2019 +0100 +++ b/modbus/modbus.py Thu May 28 11:16:59 2020 +0100 @@ -83,6 +83,7 @@ + @@ -115,7 +116,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), @@ -270,7 +293,7 @@ - + @@ -388,7 +411,7 @@ - + @@ -598,15 +621,13 @@ 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 + error_message = _("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]) + self.FatalError(error_message) + #self.GetCTRoot().logger.write_warning(error_message) + #raise Exception # Determine the current location in Beremiz's project configuration # tree @@ -720,12 +741,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 +786,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 diff -r 769fefae7c81 -r 449c9539887a runtime/BACnet_config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/BACnet_config.py Thu May 28 11:16:59 2020 +0100 @@ -0,0 +1,486 @@ +#!/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 + + + +# reference to the PLCObject in runtime/PLCObject.py +# PLCObject is a singleton, created in runtime/__init__.py +_plcobj = None + +# reference to the Nevow web server (a.k.a as NS in Beremiz_service.py) +# (Note that NS will reference the NevowServer.py _module_, and not an object/class) +_NS = None + + +# WorkingDir: the directory on which Beremiz_service.py is running, and where +# all the files downloaded to the PLC get stored +_WorkingDir = None + + +# 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 = "/tmp/BeremizBACnetConfig.json" + + + + + + +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, annotate.String), + ("port_number" , _("UDP Port Number") , ctypes.c_char_p, annotate.String), + ("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 """ + with open(os.path.realpath(_BACnetConfFilename), 'w') as f: + json.dump(BACnetConfig, f, sort_keys=True, indent=4) + global _SavedConfiguration + _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] + #_plcobj.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 _updateWebInterface(): + """ + # Add/Remove buttons to/from the web interface depending on the current state + # + # - If there is a saved state => add a delete saved state button + """ + + # Add a "Delete Saved Configuration" button if there is a saved configuration! + if _SavedConfiguration is None: + _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved") + else: + _NS.ConfigurableSettings.addSettings( + "BACnetConfigDelSaved", # name + _("BACnet Configuration"), # description + [], # fields (empty, no parameters required!) + _("Delete Configuration Stored in Persistent Storage"), # button label + OnButtonDel) # callback + + + +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! + """ + + #_plcobj.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 + + global _WebviewConfiguration + _WebviewConfiguration = newConfig + + # 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) + + # File has just been created => Delete button must be shown on web interface! + _updateWebInterface() + + + + +def OnButtonDel(**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 + # File has just been deleted => Delete button on web interface no longer needed! + _updateWebInterface() + + + +def OnButtonShowCur(**kwargs): + """ + # Function called when user clicks 'Show Current PLC Configuration' button in web interface + # The function will load the current PLC configuration into the web form + """ + + global _WebviewConfiguration + _WebviewConfiguration = _GetPLCConfiguration() + # File has just been deleted => Delete button on web interface no longer needed! + _updateWebInterface() + + + + +def OnLoadPLC(): + """ + # 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 + """ + + #_plcobj.LogMessage("BACnet web server extension::OnLoadPLC() Called...") + + if _plcobj.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 location (in the Config. Node Tree of Beremiz IDE) the BACnet plugin + # occupies in the currently loaded PLC project (i.e., the .so file) + # If the "__bacnet_plugin_location" C variable is not present in the .so file, + # we conclude that the currently loaded PLC does not have the BACnet plugin + # included (situation (2b) described above init()) + try: + location = ctypes.c_char_p.in_dll(_plcobj.PLClibraryHandle, "__bacnet_plugin_location") + except Exception: + # Loaded PLC does not have the BACnet plugin => nothing to do + # (i.e. do _not_ configure and make available the BACnet web interface) + 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: + GetParamFuncName = "__bacnet_" + location.value + "_get_ConfigParam_" + name + SetParamFuncName = "__bacnet_" + location.value + "_set_ConfigParam_" + name + + GetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, GetParamFuncName) + GetParamFuncs[name].restype = c_dtype + GetParamFuncs[name].argtypes = None + + SetParamFuncs[name] = getattr(_plcobj.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) + + # Configure the web interface to include the BACnet config parameters + _NS.ConfigurableSettings.addSettings( + "BACnetConfigParm", # name + _("BACnet Configuration"), # description + webFormInterface, # fields + _("Save Configuration to Persistent Storage"), # button label + OnButtonSave) # callback + + # Add a "View Current Configuration" button + _NS.ConfigurableSettings.addSettings( + "BACnetConfigViewCur", # name + _("BACnet Configuration"), # description + [], # fields (empty, no parameters required!) + _("Show Current PLC Configuration"), # button label + OnButtonShowCur) # callback + + # Add the Delete button to the web interface, if required + _updateWebInterface() + + + + + +def OnUnLoadPLC(): + """ + # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory + """ + + #_plcobj.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...") + + # Delete the BACnet specific web interface extensions + # (Safe to ask to delete, even if it has not been added!) + _NS.ConfigurableSettings.delSettings("BACnetConfigParm") + _NS.ConfigurableSettings.delSettings("BACnetConfigViewCur") + _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved") + GetParamFuncs = {} + SetParamFuncs = {} + _WebviewConfiguration = None + _SavedConfiguration = None + + + + +# The Beremiz_service.py service, along with the integrated web server it launches +# (i.e. Nevow web server, in runtime/NevowServer.py), will go through several states +# once started: +# (1) Web server is started, but no PLC is loaded +# (2) PLC is loaded (i.e. the PLC compiled code is loaded) +# (a) The loaded PLC includes the BACnet plugin +# (b) The loaded PLC does not have the BACnet plugin +# +# During (1) and (2a): +# we configure the web server interface to not have the BACnet web configuration extension +# During (2b) +# we configure the web server interface to include the BACnet web configuration extension +# +# plcobj : reference to the PLCObject defined in PLCObject.py +# NS : reference to the web server (i.e. the NevowServer.py module) +# WorkingDir: the directory on which Beremiz_service.py is running, and where +# all the files downloaded to the PLC get stored, including +# the .so file with the compiled C generated code +def init(plcobj, NS, WorkingDir): + #plcobj.LogMessage("BACnet web server extension::init(plcobj, NS, " + WorkingDir + ") Called") + global _WorkingDir + _WorkingDir = WorkingDir + global _plcobj + _plcobj = plcobj + global _NS + _NS = NS + global _BACnetConfFilename + if _BACnetConfFilename is None: + _BACnetConfFilename = os.path.join(WorkingDir, "BACnetConfig.json") + + _plcobj.RegisterCallbackLoad ("BACnet_Settins_Extension", OnLoadPLC) + _plcobj.RegisterCallbackUnLoad("BACnet_Settins_Extension", OnUnLoadPLC) + OnUnLoadPLC() # init is called before the PLC gets loaded... so we make sure we have the correct state diff -r 769fefae7c81 -r 449c9539887a runtime/NevowServer.py --- a/runtime/NevowServer.py Wed Dec 18 13:31:22 2019 +0100 +++ b/runtime/NevowServer.py Thu May 28 11:16:59 2020 +0100 @@ -179,7 +179,13 @@ setattr(self, 'action_' + name, callback) - self.bindingsNames.append(name) + if name not in self.bindingsNames: + self.bindingsNames.append(name) + + + def delSettings(self, name): + if name in self.bindingsNames: + self.bindingsNames.remove(name) ConfigurableSettings = ConfigurableBindings() diff -r 769fefae7c81 -r 449c9539887a runtime/PLCObject.py --- a/runtime/PLCObject.py Wed Dec 18 13:31:22 2019 +0100 +++ b/runtime/PLCObject.py Thu May 28 11:16:59 2020 +0100 @@ -99,6 +99,9 @@ self.TraceLock = Lock() self.Traces = [] self.DebugToken = 0 + # Callbacks used by web settings extensions (e.g.: BACnet_config.py, Modbus_config.py) + self.LoadCallbacks = {} # list of functions to call when PLC is loaded + self.UnLoadCallbacks = {} # list of functions to call when PLC is unloaded self._init_blobs() @@ -167,6 +170,22 @@ return self._loading_error, 0, 0, 0 return None + def RegisterCallbackLoad(self, ExtensionName, ExtensionCallback): + """ + Register function to be called when PLC is loaded + ExtensionName: a string with the name of the extension asking to register the callback + ExtensionCallback: the function to be called... + """ + self.LoadCallbacks[ExtensionName] = ExtensionCallback + + def RegisterCallbackUnLoad(self, ExtensionName, ExtensionCallback): + """ + Register function to be called when PLC is unloaded + ExtensionName: a string with the name of the extension asking to register the callback + ExtensionCallback: the function to be called... + """ + self.UnLoadCallbacks[ExtensionName] = ExtensionCallback + def _GetMD5FileName(self): return os.path.join(self.workingdir, "lasttransferedPLC.md5") @@ -270,6 +289,8 @@ res = self._LoadPLC() if res: self.PythonRuntimeInit() + for name, callbackFunc in self.LoadCallbacks.items(): + callbackFunc() else: self._FreePLC() @@ -278,6 +299,8 @@ @RunInMain def UnLoadPLC(self): self.PythonRuntimeCleanup() + for name, callbackFunc in self.UnLoadCallbacks.items(): + callbackFunc() self._FreePLC() def _InitPLCStubCalls(self):