# HG changeset patch # User Edouard Tisserant # Date 1600335022 -7200 # Node ID 6ea4b7e1a9ed5c25dfd070227118507b42cf5852 # Parent 6dd617cc9c050afc57ea631d23d071e965609a2d# Parent 696301e869d56c1acb009346c93a6b88deb28f4f Merge + fix side effects of making warning instead of errors in case of missing HMI variable diff -r 696301e869d5 -r 6ea4b7e1a9ed docutil/docsvg.py --- a/docutil/docsvg.py Wed Sep 16 09:42:26 2020 +0200 +++ b/docutil/docsvg.py Thu Sep 17 11:30:22 2020 +0200 @@ -34,16 +34,23 @@ if wx.Platform == '__WXMSW__': from six.moves import winreg try: - svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, - 'Software\\Classes\\svgfile\\shell\\Inkscape\\command') + svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, + 'Software\\Classes\\svgfile\\shell\\Inkscape\\command') except OSError: - svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, - 'Software\\Classes\\inkscape.svg\\shell\\open\\command') + try: + svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, + 'Software\\Classes\\inkscape.svg\\shell\\open\\command') + except Exception: + return None + svgexepath = svgexepath.replace('"%1"', '').strip() return svgexepath.replace('"', '') else: - # TODO: search path - return os.path.join("/usr/bin", "inkscape") + # TODO: search for inkscape in $PATH + svgexepath = os.path.join("/usr/bin", "inkscape") + if os.path.exists(svgexepath): + return svgexepath + return None def open_win_svg(svgexepath, svgfile): diff -r 696301e869d5 -r 6ea4b7e1a9ed modbus/mb_runtime.c --- a/modbus/mb_runtime.c Wed Sep 16 09:42:26 2020 +0200 +++ b/modbus/mb_runtime.c Thu Sep 17 11:30:22 2020 +0200 @@ -28,6 +28,7 @@ #include #include #include +#include /* required for pause() */ #include "mb_slave_and_master.h" #include "MB_%(locstr)s.h" @@ -328,7 +329,7 @@ /* struct timespec cur_time; clock_gettime(CLOCK_MONOTONIC, &cur_time); - fprintf(stderr, "Modbus client thread - new cycle (%%ld:%%ld)!\n", cur_time.tv_sec, cur_time.tv_nsec); + fprintf(stderr, "Modbus client thread (%%d) - new cycle (%%ld:%%ld)!\n", client_node_id, cur_time.tv_sec, cur_time.tv_nsec); */ int req; for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){ @@ -345,8 +346,10 @@ 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 ); + /* + fprintf(stderr, "Modbus client thread (%%d): RUNNING Modbus request %%d (periodic = %%d flag_exec_req = %%d)\n", + client_node_id, 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) { @@ -433,102 +436,76 @@ -/* 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. + + +/* Thread that simply implements a periodic 'timer', + * i.e. periodically sends signal to the thread running __mb_client_thread() * - * 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. + * Note that we do not use a posix timer (timer_create() ) because there doesn't seem to be a way + * of having the timer notify the thread that is portable across Xenomai and POSIX. + * - SIGEV_THREAD : not supported by Xenomai + * - SIGEV_THREAD_ID : Linux specific (i.e. non POSIX) + * Even so, I did not get it to work under Linux (issues with the header files) + * - SIGEV_SIGNAL : Will not work, as signal is sent to random thread in process! */ -void __client_node_timer_callback_function(int client_node_id) { - /* 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() */ - if (pthread_mutex_trylock (&(client_nodes[client_node_id].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. +static void *__mb_client_timer_thread(void *_index) { + int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast) + struct timespec next_cycle; + + int period_sec = client_nodes[client_node_id].comm_period / 1000; /* comm_period is in ms */ + int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */ + + // Enable thread cancelation. Enabled is default, but set it anyway to be safe. + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + + if (client_nodes[client_node_id].comm_period <= 0) { + // No periodic activation required => nothing to do! + while (1) pause(); // wait to be canceled when program terminates (shutdown() is called) + return NULL; // not really necessary, just makes it easier to understand the code. + } + + // get the current time + clock_gettime(CLOCK_MONOTONIC, &next_cycle); + + while(1) { + // 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: + * It is probably un-necessary to check for overflow of timer! + * 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. */ - return; - client_nodes[client_node_id].execute_req = 1; // tell the thread to execute - client_nodes[client_node_id].periodic_act = 1; // tell the thread the activation was done by periodic timer - pthread_cond_signal (&(client_nodes[client_node_id].condv)); - pthread_mutex_unlock(&(client_nodes[client_node_id].mutex)); -} - - - -static int stop_mb_client_timer_thread; -static void *__mb_client_timer_thread(void *_index) { - sigset_t set; - int signum; - sigemptyset(&set); - sigaddset(&set, SIGALRM); - - int client_node_id = (char *)_index - (char *)NULL; // Use pointer arithmetic (more portable than cast) - /* 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[client_node_id].periodic_act = 0; - - if (timer_create(CLOCK_REALTIME, NULL, &(client_nodes[client_node_id].timer_id)) < 0) { - fprintf(stderr, "Modbus plugin: Error (%%s) creating timer for modbus client node %%s\n", strerror(errno), client_nodes[client_node_id].location); - return NULL; + clock_gettime(CLOCK_MONOTONIC, &now); + if (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); + } + + while (0 != clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL)); + + /* 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() */ + if (pthread_mutex_trylock (&(client_nodes[client_node_id].mutex)) == 0) { + client_nodes[client_node_id].execute_req = 1; // tell the thread to execute + client_nodes[client_node_id].periodic_act = 1; // tell the thread the activation was done by periodic timer + pthread_cond_signal (&(client_nodes[client_node_id].condv)); + pthread_mutex_unlock(&(client_nodes[client_node_id].mutex)); + } else { + /* 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. + */ } } - int period_sec = client_nodes[client_node_id].comm_period / 1000; /* comm_period is in ms */ - int period_nsec = (client_nodes[client_node_id].comm_period %%1000)*1000000; /* comm_period is in ms */ - - // 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); - } - - stop_mb_client_timer_thread = 0; - while(!stop_mb_client_timer_thread) { - if(sigwait (&set, &signum) == -1) - perror ("sigwait"); - - if(stop_mb_client_timer_thread) - break; - - if(signum == SIGALRM) - __client_node_timer_callback_function(client_node_id); - else - fprintf(stderr, "Modbus plugin: spurious wakeup of timer thread for Modbus client %%s.\n", client_nodes[client_node_id].location); - - } - - // timer was created, so we try to destroy it! - int res = timer_delete(client_nodes[client_node_id].timer_id); - if (res < 0) - fprintf(stderr, "Modbus plugin: Error destroying timer for modbus client node %%s\n", client_nodes[client_node_id].location); - - return NULL; + return NULL; // humour the compiler -> will never be executed! } @@ -740,13 +717,26 @@ */ 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; - } + + /* 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)); + /* - 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; + } else { + /* The mutex is locked => the client thread is currently executing MB transactions. + * We will try to activate it in the next PLC cycle... + * For now, do nothing. + */ + } } } } @@ -794,8 +784,8 @@ close = 0; if (client_nodes[index].init_state >= 4) { - stop_mb_client_timer_thread = 1; - pthread_kill(client_nodes[index].timer_thread_id, SIGALRM); + // timer thread was launched, so we try to cancel it! + close = pthread_cancel(client_nodes[index].timer_thread_id); close |= pthread_join (client_nodes[index].timer_thread_id, NULL); if (close < 0) fprintf(stderr, "Modbus plugin: Error closing timer thread for modbus client node %%s\n", client_nodes[index].location); @@ -835,6 +825,7 @@ client_nodes[index].init_state = 0; } +//fprintf(stderr, "Modbus plugin: __cleanup_%%s() 5 close=%%d res=%%d\n", client_nodes[index].location, close, res); /* kill thread and close connections of each modbus server node */ for (index=0; index < NUMBER_OF_SERVER_NODES; index++) { close = 0; diff -r 696301e869d5 -r 6ea4b7e1a9ed modbus/mb_runtime.h --- a/modbus/mb_runtime.h Wed Sep 16 09:42:26 2020 +0200 +++ b/modbus/mb_runtime.h Thu Sep 17 11:30:22 2020 +0200 @@ -107,7 +107,6 @@ int prev_error; // error code of the last printed error message (0 when no error) pthread_t thread_id; // thread handling all communication for this client node pthread_t timer_thread_id; // thread handling periodical timer 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, diff -r 696301e869d5 -r 6ea4b7e1a9ed modbus/web_settings.py --- a/modbus/web_settings.py Wed Sep 16 09:42:26 2020 +0200 +++ b/modbus/web_settings.py Thu Sep 17 11:30:22 2020 +0200 @@ -183,7 +183,7 @@ ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ), ("parity" , _("Parity") , ctypes.c_int, MB_Parity ), ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ), - ("slave_id" , _("Slave ID") , ctypes.c_ulonglong, annotate.Integer) + ("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer) ] @@ -228,7 +228,7 @@ # This allows us to confirm the saved data contains the correct addr_type # when loading from file save_info = {} - save_info["addr_type"] = ["addr_type"] + save_info["addr_type"] = WebNode_entry["addr_type"] save_info["node_type"] = WebNode_entry["node_type"] save_info["config" ] = newConfig @@ -262,7 +262,7 @@ filename = _WebNodeList[WebNode_id]["filename"] try: #if os.path.isfile(filename): - save_info = json.load(open(filename)) + save_info = json.load(open(os.path.realpath(filename))) except Exception: return None diff -r 696301e869d5 -r 6ea4b7e1a9ed plcopen/plcopen.py --- a/plcopen/plcopen.py Wed Sep 16 09:42:26 2020 +0200 +++ b/plcopen/plcopen.py Thu Sep 17 11:30:22 2020 +0200 @@ -331,12 +331,16 @@ def SaveProject(project, filepath): - project_file = open(filepath, 'w') - project_file.write(etree.tostring( + content = etree.tostring( project, pretty_print=True, xml_declaration=True, - encoding='utf-8')) + encoding='utf-8') + + assert len(content) != 0 + + project_file = open(filepath, 'w') + project_file.write(content) project_file.close() diff -r 696301e869d5 -r 6ea4b7e1a9ed runtime/PLCObject.py --- a/runtime/PLCObject.py Wed Sep 16 09:42:26 2020 +0200 +++ b/runtime/PLCObject.py Thu Sep 17 11:30:22 2020 +0200 @@ -440,6 +440,7 @@ if cmd == "Activate": self.PythonRuntimeCall("start") + self.PreStartPLC() self.PythonThreadLoop() self.PythonRuntimeCall("stop", reverse_order=True) else: # "Finish" @@ -471,8 +472,6 @@ if not self.LoadPLC(): self._fail(_("Problem starting PLC : can't load PLC")) - self.PreStartPLC() - if self.CurrentPLCFilename is not None and self.PLCStatus == PlcStatus.Stopped: c_argv = ctypes.c_char_p * len(self.argv) res = self._startPLC(len(self.argv), c_argv(*self.argv)) diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/gen_index_xhtml.xslt Thu Sep 17 11:30:22 2020 +0200 @@ -1,9 +1,9 @@ - - - - - + + + + + HMI_PLC_STATUS @@ -12,11 +12,11 @@ HMI_CURRENT_PAGE - + - + @@ -63,8 +63,8 @@ - - + + @@ -96,7 +96,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -114,16 +114,16 @@ - + - - - + + + @@ -134,7 +134,7 @@ - + @@ -160,15 +160,15 @@ - + - - + + @@ -202,7 +202,7 @@ - + @@ -215,12 +215,12 @@ - - + + - + @@ -266,7 +266,7 @@ - + @@ -301,8 +301,8 @@ - - + + @@ -324,10 +324,10 @@ - + - + @@ -337,13 +337,13 @@ - - - + + + - - + + @@ -383,13 +383,13 @@ - - + + - - - + + + @@ -401,9 +401,9 @@ - - - + + + @@ -417,13 +417,13 @@ - - + + - - - + + + @@ -437,8 +437,8 @@ - - + + @@ -472,18 +472,18 @@ - - - + + + - - - - - - - - + + + + + + + + " ": { @@ -540,10 +540,10 @@ jumps: [ - + - + @@ -576,7 +576,7 @@ } - + } @@ -637,7 +637,10 @@ - + + + + @@ -670,23 +673,26 @@ All units must be set to "px" in Inkscape's document properties - - - - + + + + + + + + + - - + - + - - + - + @@ -711,7 +717,7 @@ id - + transform @@ -720,16 +726,30 @@ style - + - - - + + + + + + + _ + + + + + + + + + + @@ -756,7 +776,7 @@ - + @@ -766,7 +786,7 @@ - + @@ -779,15 +799,31 @@ _ + + + - - - + + + + + + + + + + + + + + + + @@ -802,7 +838,7 @@ - + @@ -811,7 +847,7 @@ - + @@ -861,8 +897,8 @@ - - + + " @@ -879,7 +915,7 @@ - + Widget id=" @@ -888,6 +924,10 @@ " in HMI tree + undefined + + , + " @@ -929,7 +969,7 @@ ],{ - + }) @@ -942,7 +982,7 @@ - + @@ -1143,6 +1183,8 @@ let index = this.get_variable_index(i); + if(index == undefined) continue; + subscribers(index).add(this); } @@ -1161,6 +1203,8 @@ let realindex = this.get_variable_index(index); + if(realindex == undefined) continue; + let cached_val = cache[realindex]; if(cached_val != undefined) @@ -1195,9 +1239,13 @@ } - change_hmi_value(index,opstr) { - - return change_hmi_value(this.get_variable_index(index), opstr); + change_hmi_value(index, opstr) { + + let realindex = this.get_variable_index(index); + + if(realindex == undefined) return undefined; + + return change_hmi_value(realindex, opstr); } @@ -1205,7 +1253,11 @@ apply_hmi_value(index, new_val) { - return apply_hmi_value(this.get_variable_index(0), new_val); + let realindex = this.get_variable_index(index); + + if(realindex == undefined) return undefined; + + return apply_hmi_value(realindex, new_val); } @@ -1219,6 +1271,8 @@ let refindex = this.get_variable_index(i); + if(refindex == undefined) continue; + if(index == refindex) { @@ -1298,7 +1352,7 @@ - + @@ -1315,8 +1369,8 @@ } - - + + @@ -1336,14 +1390,14 @@ - - - + + + - + - - + + @@ -1368,8 +1422,8 @@ _sub: { - - + + @@ -1454,12 +1508,26 @@ state = 0; + plc_lock = false; + active_style = undefined; inactive_style = undefined; + dispatch(value) { + + if(value){ + + this.button_release(); + + } + + } + + + on_mouse_down(evt) { if (this.active_style && this.inactive_style) { @@ -1472,21 +1540,41 @@ this.apply_hmi_value(0, 1); + this.plc_lock = false; + } on_mouse_up(evt) { - if (this.active_style && this.inactive_style) { - - this.active_elt.setAttribute("style", "display:none"); - - this.inactive_elt.setAttribute("style", this.inactive_style); - - } - - this.apply_hmi_value(0, 0); + this.button_release(); + + } + + + + button_release(){ + + if(!this.plc_lock){ + + this.plc_lock = true; + + } + + else{ + + if (this.active_style && this.inactive_style) { + + this.active_elt.setAttribute("style", "display:none"); + + this.inactive_elt.setAttribute("style", this.inactive_style); + + } + + this.apply_hmi_value(0, 0); + + } } @@ -1522,11 +1610,11 @@ - + active inactive - + @@ -1624,17 +1712,17 @@ - + path - + value min max - + @@ -2028,17 +2116,17 @@ - + handle range - + value min max - + @@ -2052,8 +2140,6 @@ this.fields[index] = value; - console.log(value, index); - this.element.textContent = this.args.length == 1 ? vsprintf(this.args[0],this.fields) : this.fields.join(' '); } @@ -2570,7 +2656,7 @@ - + text box button @@ -2891,8 +2977,6 @@ } - console.log(this.menu_offset); - this.set_partial_text(); }, @@ -3096,13 +3180,13 @@ must have one argument given : a class name. - - - - - - - + + + + + + + index_pool: [ @@ -3118,11 +3202,11 @@ init: function() { - - - + + + - + id(" ").setAttribute("onclick", "hmi_widgets[' @@ -3136,13 +3220,13 @@ this.items = [ - - + + - - - - + + + + [ /* item=" " path=" @@ -3333,22 +3417,22 @@ - + key_pos - + - + value - + - + frequency: 5, @@ -3366,7 +3450,7 @@ }, - + init: function() { @@ -3403,7 +3487,7 @@ ", " - ", this, this.last_val,size); + ", this, this.last_val, size); }, @@ -3417,15 +3501,19 @@ class JsonTableWidget extends Widget{ - do_http_request() { + cache = []; + + do_http_request(...opt) { const query = { - offset: '42', - - filter: '*powerloss*', - - args: this.args + args: this.args, + + vars: this.cache, + + visible: this.visible, + + options: opt }; @@ -3439,7 +3527,7 @@ headers: {'Content-Type': 'application/json'} - } + }; @@ -3447,27 +3535,23 @@ .then(res => res.json()) - .then(this.spread_json_data); + .then(this.spread_json_data.bind(this)); } - dispatch(value) { + dispatch(value, oldval, index) { + + this.cache[index] = value; this.do_http_request(); } - on_click(evt) { - - this.do_http_request(); - - } - - init() { - - this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + on_click(evt, ...options) { + + this.do_http_request(...options); } @@ -3481,13 +3565,83 @@ . - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JsonTable : missplaced '=' or inconsistent names in Json data expressions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jdata + + + + - - - - + + + id(" @@ -3497,29 +3651,13 @@ "#"+hmi_widgets[" "].items[ - - ]); - - - - console.log("from_textsylelist"," - - ", " - - ", - - , - - hmi_widgets[" - - "].items[ - + ]); - Clones (svg:use) in JsonTable Widget must point to a valid HMI:List or HMI:TextStyleList widget or item. Reference " + Clones (svg:use) in JsonTable Widget must point to a valid HMI:List widget or item. Reference " " is not valid and will not be updated. @@ -3527,59 +3665,194 @@ - - id(" + + + + + + + + + + Clones (svg:use) in JsonTable Widget pointing to a HMI:TextStyleList widget or item must have a "textContent=.someVal" assignement following value expression in label. + + + { + + let elt = id(" + + "); + + elt.textContent = String( + + ); + + elt.style = hmi_widgets[" + + "].styles[ + + ]; + + } + + + + id(" + + ").textContent = String( + + ); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id(" + + ").setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt, ' + + ', '"+ + + +"')"); + + + + + + + + + + + + + obj_ + + _ + + try { + + + let + + + = + + ; + + if( + + + == undefined) { + + console.log(" + + + = + + "); + + throw null; + + } + + + + + + + + + + + + + + id(" - ").textContent = String( - - ); - - - - - - - - - + ").setAttribute("style", " + + "); + + + + - - - - let obj_ - - = - - - ; - - - - obj_ - - - + } catch(err) { + + id(" + + ").setAttribute("style", "display:none"); + + } + - + data - + forward backward cursor - + - - spread_json_data: function(jdata) { - - - + + visible: + + , + + spread_json_data: function(janswer) { + + let [range,position,jdata] = janswer; + + this.apply_hmi_value(1, range); + + this.apply_hmi_value(2, position); + + console.log(range,position,jdata); + + + + } @@ -3587,38 +3860,38 @@ - + active inactive - + - + disabled - + - + - + - + - + active: false, @@ -3821,9 +4094,9 @@ var keypads = { - + - + " ":[" @@ -4020,11 +4293,25 @@ on_Enter_click() { - end_modal.call(this); - - let callback_obj = this.result_callback_obj; - - callback_obj.edit_callback(this.editstr); + let coercedval = (typeof this.initial) == "number" ? Number(this.editstr) : this.editstr; + + if(typeof coercedval == 'number' && isNaN(coercedval)){ + + // revert to initial so it explicitely shows input was ignored + + this.editstr = String(this.initial); + + this.update(); + + } else { + + let callback_obj = this.result_callback_obj; + + end_modal.call(this); + + callback_obj.edit_callback(coercedval); + + } } @@ -4118,7 +4405,7 @@ show_modal.call(this,size); - this.editstr = initial; + this.editstr = String(initial); this.result_callback_obj = callback_obj; @@ -4128,6 +4415,10 @@ this.caps = false; + this.initial = initial; + + + this.update(); } @@ -4168,25 +4459,25 @@ - + Esc Enter BackSpace Keys Info Value - + Sign Space NumDot position - + - + CapsLock Shift - - + + init: function() { @@ -4232,7 +4523,7 @@ - + coordinates: [ , @@ -4240,13 +4531,13 @@ ], - + items: { - + : " ", @@ -4255,6 +4546,22 @@ }, + + + styles: { + + + + + + : " + + ", + + + }, + + class MeterWidget extends Widget{ @@ -4312,17 +4619,17 @@ - + needle range - + value min max - + @@ -4413,9 +4720,9 @@ choices: [ - + - + { elt:id(" @@ -4447,47 +4754,163 @@ range = undefined; + handle_orig = undefined; + + scroll_size = 10; + + min_size = 0.07; + fi = undefined; - svg_dist = undefined; + curr_value = 0; drag = false; enTimer = false; + handle_click = undefined; + + last_drag = false; + dispatch(value) { + //save current value inside widget + + this.curr_value = value; + + + if(this.value_elt) this.value_elt.textContent = String(value); - this.update_DOM(value, this.handle_elt); - - + //don't update if draging and setpoint ghost doesn't exist + + if(!this.drag || (this.setpoint_elt != undefined)){ + + this.update_DOM(value, this.handle_elt); + + } } - last_drag = false; - - - update_DOM(value, elt){ let [min,max,start,totallength] = this.range; - let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min))); - - let tip = this.range_elt.getPointAtLength(length); - - elt.setAttribute('transform',"translate("+(tip.x-start.x)+","+(tip.y-start.y)+")"); - - + // check if handle is resizeable + + if (this.scroll_size != undefined){ //size changes + + //get parameters + + let length = Math.max(min,Math.min(max,(Number(value)-min)*max/(max-min))); + + let tip = this.range_elt.getPointAtLength(length); + + let handle_min = totallength*this.min_size; + + + + let step = 1; + + //check if range is bigger than max displayed and recalculate step + + if ((totallength/handle_min) < (max-min+1)){ + + step = (max-min+1)/(totallength/handle_min-1); + + } + + + + let kx,ky,offseY,offseX = undefined; + + //scale on x or y axes + + if (this.fi > 0.75){ + + //get scale factor + + if(step > 1){ + + ky = handle_min/this.handle_orig.height; + + } + + else{ + + ky = (totallength-handle_min*(max-min))/this.handle_orig.height; + + } + + kx = 1; + + //get 0 offset to stay inside range + + offseY = start.y - (this.handle_orig.height + this.handle_orig.y) * ky; + + offseX = 0; + + //get distance from value + + tip.y =this.range_elt.getPointAtLength(0).y - length/step *handle_min; + + } + + else{ + + //get scale factor + + if(step > 1){ + + kx = handle_min/this.handle_orig.width; + + } + + else{ + + kx = (totallength-handle_min*(max-min))/this.handle_orig.width; + + } + + ky = 1; + + //get 0 offset to stay inside range + + offseX = start.x - (this.handle_orig.x * kx); + + offseY = 0; + + //get distance from value + + tip.x =this.range_elt.getPointAtLength(0).x + length/step *handle_min; + + } + + elt.setAttribute('transform',"matrix("+(kx)+" 0 0 "+(ky)+" "+(tip.x-start.x+offseX)+" "+(tip.y-start.y+offseY)+")"); + + } + + else{ //size stays the same + + let length = Math.max(0,Math.min(totallength,(Number(value)-min)*totallength/(max-min))); + + let tip = this.range_elt.getPointAtLength(length); + + elt.setAttribute('transform',"translate("+(tip.x-start.x)+","+(tip.y-start.y)+")"); + + } + + + + // show or hide ghost if exists if(this.setpoint_elt != undefined){ @@ -4515,6 +4938,8 @@ on_release(evt) { + //unbind events + window.removeEventListener("touchmove", this.on_bound_drag, true); window.removeEventListener("mousemove", this.on_bound_drag, true); @@ -4527,26 +4952,38 @@ window.removeEventListener("touchcancel", this.bound_on_release, true); + + + //reset drag flag + if(this.drag){ this.drag = false; } + + + // get final position + this.update_position(evt); + + } - - on_drag(evt){ + //ignore drag event for X amount of time and if not selected + if(this.enTimer && this.drag){ this.update_position(evt); + + //reset timer this.enTimer = false; @@ -4563,16 +5000,18 @@ var html_dist = 0; + let [min,max,start,totallength] = this.range; + //calculate size of widget in html var range_borders = this.range_elt.getBoundingClientRect(); + var [minX,minY,maxX,maxY] = [range_borders.left,range_borders.bottom,range_borders.right,range_borders.top]; + var range_length = Math.sqrt( range_borders.height*range_borders.height + range_borders.width*range_borders.width ); - var [minX,minY,maxX,maxY] = [range_borders.left,range_borders.bottom,range_borders.right,range_borders.top]; - //get range and mouse coordinates @@ -4599,112 +5038,240 @@ - //get handle distance from mouse position - - if (minX > mouseX && minY < mouseY){ - - html_dist = 0; + // calculate position + + if (this.handle_click){ //if clicked on handle + + let moveDist = 0, resizeAdd = 0; + + let range_percent = 1; + + + + //set paramters for resizeable handle + + if (this.scroll_size != undefined){ + + // add one more object to stay inside range + + resizeAdd = 1; + + + + //chack if range is bigger than display option and + + // calculate percent of range with out handle + + if(((max/(max*this.min_size)) < (max-min+1))){ + + range_percent = 1-this.min_size; + + } + + else{ + + range_percent = 1-(max-max*this.min_size*(max-min))/max; + + } + + } + + + + //calculate value difference on x or y axis + + if(this.fi > 0.7){ + + moveDist = ((max-min+resizeAdd)/(range_length*range_percent))*((this.handle_click[1]-mouseY)/Math.sin(this.fi)); + + } + + else{ + + moveDist = ((max-min+resizeAdd)/(range_length*range_percent))*((mouseX-this.handle_click[0])/Math.cos(this.fi)); + + } + + + + this.curr_value = Math.ceil(this.handle_click[2] + moveDist); } - else if (maxX < mouseX && maxY > mouseY){ - - html_dist = range_length; + else{ //if clicked on widget + + //get handle distance from mouse position + + if (minX > mouseX && minY < mouseY){ + + html_dist = 0; + + } + + else if (maxX < mouseX && maxY > mouseY){ + + html_dist = range_length; + + } + + else{ + + if(this.fi > 0.7){ + + html_dist = (minY - mouseY)/Math.sin(this.fi); + + } + + else{ + + html_dist = (mouseX - minX)/Math.cos(this.fi); + + } + + } + + //calculate distance + + this.curr_value=Math.ceil((html_dist/range_length)*(this.range[1]-this.range[0])+this.range[0]); } + + + //check if in range + + if (this.curr_value > max){ + + this.curr_value = max; + + } + + else if (this.curr_value < min){ + + this.curr_value = min; + + } + + + + this.apply_hmi_value(0, this.curr_value); + + + + //redraw handle + + this.request_animate(); + + + + } + + + + animate(){ + + // redraw handle on screen refresh + + // check if setpoint(ghost) handle exsist otherwise update main handle + + if(this.setpoint_elt != undefined){ + + this.update_DOM(this.curr_value, this.setpoint_elt); + + } + else{ - // calculate distace - - if(this.fi > 0.7){ - - html_dist = (minY - mouseY)/Math.sin(this.fi); + this.update_DOM(this.curr_value, this.handle_elt); + + } + + } + + + + on_select(evt){ + + //enable drag flag and timer + + this.drag = true; + + this.enTimer = true; + + + + //bind events + + window.addEventListener("touchmove", this.on_bound_drag, true); + + window.addEventListener("mousemove", this.on_bound_drag, true); + + + + window.addEventListener("mouseup", this.bound_on_release, true) + + window.addEventListener("touchend", this.bound_on_release, true); + + window.addEventListener("touchcancel", this.bound_on_release, true); + + + + // check if handle was pressed + + if (evt.currentTarget == this.handle_elt){ + + //get mouse position on the handle + + let mouseX = undefined; + + let mouseY = undefined; + + if (evt.type.startsWith("touch")){ + + mouseX = Math.ceil(evt.touches[0].clientX); + + mouseY = Math.ceil(evt.touches[0].clientY); } else{ - html_dist = (mouseX - minX)/Math.cos(this.fi); + mouseX = evt.pageX; + + mouseY = evt.pageY; } - - - //check if in range - - if (html_dist > range_length){ - - html_dist = range_length; - - } - - else if (html_dist < 0){ - - html_dist = 0; - - } - - + //save coordinates and orig value + + this.handle_click = [mouseX,mouseY,this.curr_value]; } - - - this.svg_dist=Math.ceil((html_dist/range_length)*this.range[1]); - - - - this.apply_hmi_value(0, this.svg_dist); - - - - // update ghost cursor - - if(this.setpoint_elt != undefined){ - - this.request_animate(); + else{ + + // get new handle position and reset if handle was not pressed + + this.handle_click = undefined; + + this.update_position(evt); } + + + //prevent next events + + evt.stopPropagation(); + } - animate(){ - - this.update_DOM(this.svg_dist, this.setpoint_elt); - - } - - - - on_select(evt){ - - this.drag = true; - - this.enTimer = true; - - window.addEventListener("touchmove", this.on_bound_drag, true); - - window.addEventListener("mousemove", this.on_bound_drag, true); - - - - window.addEventListener("mouseup", this.bound_on_release, true) - - window.addEventListener("touchend", this.bound_on_release, true); - - window.addEventListener("touchcancel", this.bound_on_release, true); - - this.update_position(evt); - - } - - - init() { + //set min max value if not defined + let min = this.min_elt ? Number(this.min_elt.textContent) : @@ -4719,6 +5286,10 @@ + // save initial parameters + + this.range_elt.style.strokeMiterlimit="0"; + this.range = [min, max, this.range_elt.getPointAtLength(0),this.range_elt.getTotalLength()]; let start = this.range_elt.getPointAtLength(0); @@ -4727,7 +5298,11 @@ this.fi = Math.atan2(start.y-end.y, end.x-start.x); - + this.handle_orig = this.handle_elt.getBBox(); + + + + //bind functions this.bound_on_select = this.on_select.bind(this); @@ -4737,6 +5312,8 @@ + this.handle_elt.addEventListener("mousedown", this.bound_on_select); + this.element.addEventListener("mousedown", this.bound_on_select); this.element.addEventListener("touchstart", this.bound_on_select); @@ -4761,17 +5338,17 @@ - + handle range - + value min max setpoint - + @@ -4806,9 +5383,9 @@ choices: [ - + - + { elt:id(" @@ -4848,38 +5425,56 @@ dispatch(value) { - this.state = value; + if(this.state != value){ + + this.state = value; + + if (this.state) { + + this.active_elt.setAttribute("style", this.active_style); + + this.inactive_elt.setAttribute("style", "display:none"); + + } else { + + this.inactive_elt.setAttribute("style", this.inactive_style); + + this.active_elt.setAttribute("style", "display:none"); + + } + + } + + } + + + + on_click(evt) { if (this.state) { + this.inactive_elt.setAttribute("style", this.inactive_style); + + this.active_elt.setAttribute("style", "display:none"); + + this.state = 0; + + } else { + this.active_elt.setAttribute("style", this.active_style); this.inactive_elt.setAttribute("style", "display:none"); - this.state = 0; - - } else { - - this.inactive_elt.setAttribute("style", this.inactive_style); - - this.active_elt.setAttribute("style", "display:none"); - this.state = 1; } + this.apply_hmi_value(0, this.state); + } - on_click(evt) { - - this.apply_hmi_value(0, this.state); - - } - - - init() { this.active_style = this.active_elt.style.cssText; @@ -4888,6 +5483,10 @@ this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + this.inactive_elt.setAttribute("style", this.inactive_style); + + this.active_elt.setAttribute("style", "display:none"); + } } @@ -4896,7 +5495,7 @@ - + active inactive @@ -5432,8 +6031,6 @@ if(index > last_remote_index){ - console.log("updated local variable ",index,value); - updates[index] = value; requestHMIAnimation(); @@ -5784,8 +6381,6 @@ let [keypadid, xcoord, ycoord] = keypads[valuetype]; - console.log('XXX TODO : Edit value', path, valuetype, callback, initial, keypadid); - edit_callback = callback; let widget = hmi_widgets[keypadid]; @@ -5874,8 +6469,6 @@ eltsub.active.setAttribute("style", eltsub.active_style); - console.log("active", eltsub); - }; function widget_inactive_activable(eltsub) { @@ -5890,8 +6483,6 @@ eltsub.inactive.setAttribute("style", eltsub.inactive_style); - console.log("inactive", eltsub); - }; diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/inline_svg.ysl2 --- a/svghmi/inline_svg.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/inline_svg.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -10,7 +10,10 @@ // Identity template : // - copy every attributes // - copy every sub-elements -template "@* | node()", mode="inline_svg" { + +svgtmpl "@*", mode="inline_svg" xsl:copy; + +template "node()", mode="inline_svg" { // use real xsl:copy instead copy-of alias from yslt.yml2 if "not(@id = $discardable_elements/@id)" xsl:copy apply "@* | node()", mode="inline_svg"; @@ -51,19 +54,22 @@ const "hmi_lists", "$hmi_elements[@id = $hmi_lists_descs/@id]"; const "targets_not_to_unlink", "$hmi_lists/descendant-or-self::svg:*"; const "to_unlink", "$hmi_elements[not(@id = $hmi_pages/@id)]/descendant-or-self::svg:use"; -svgtmpl "svg:use", mode="inline_svg" -{ - param "seed"; + +def "func:is_unlinkable" { + param "targetid"; + param "eltid"; + result "$eltid = $to_unlink/@id and not($targetid = $targets_not_to_unlink/@id)"; +} + +svgtmpl "svg:use", mode="inline_svg"{ const "targetid","substring-after(@xlink:href,'#')"; choose { - when "@id = $to_unlink/@id and not($targetid = $targets_not_to_unlink/@id)" { + when "func:is_unlinkable($targetid, @id)" { call "unlink_clone" { with "targetid", "$targetid"; - with "seed","$seed"; - } - } - otherwise - xsl:copy apply "@* | node()", mode="inline_svg"; + } + } + otherwise xsl:copy apply "@*", mode="inline_svg"; } } @@ -87,11 +93,16 @@ svgfunc "unlink_clone"{ param "targetid"; - param "seed"; + param "seed","''"; const "target", "//svg:*[@id = $targetid]"; - const "seeded_id","concat($seed, @id)"; + const "seeded_id" choose { + when "string-length($seed) > 0" > «$seed»_«@id» + otherwise value "@id"; + } g{ attrib "id" value "$seeded_id"; + attrib "original" value "@id"; + choose { when "$target[self::svg:g]" { foreach "@*[not(local-name() = $excluded_use_attrs/name | $merge_use_attrs)]" @@ -112,7 +123,7 @@ } apply "$target/*", mode="unlink_clone"{ - with "seed","concat($seed, @id)"; + with "seed","$seeded_id"; } } otherwise { @@ -121,7 +132,7 @@ attrib "{name()}" > «.» apply "$target", mode="unlink_clone"{ - with "seed","concat($seed, @id)"; + with "seed","$seeded_id"; } } } @@ -133,13 +144,25 @@ svgtmpl "@id", mode="unlink_clone" { param "seed"; attrib "id" > «$seed»_«.» + attrib "original" > «.» } svgtmpl "@*", mode="unlink_clone" xsl:copy; svgtmpl "svg:use", mode="unlink_clone" { param "seed"; - apply "." mode="inline_svg" with "seed","concat($seed, '_')"; + const "targetid","substring-after(@xlink:href,'#')"; + choose { + when "func:is_unlinkable($targetid, @id)" { + call "unlink_clone" { + with "targetid", "$targetid"; + with "seed","$seed"; + } + } + otherwise xsl:copy apply "@*", mode="unlink_clone" { + with "seed","$seed"; + } + } } // copying widgets would have unwanted effect diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/svghmi.js --- a/svghmi/svghmi.js Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/svghmi.js Thu Sep 17 11:30:22 2020 +0200 @@ -240,7 +240,6 @@ function send_hmi_value(index, value) { if(index > last_remote_index){ - console.log("updated local variable ",index,value); updates[index] = value; requestHMIAnimation(); return; @@ -416,7 +415,6 @@ function edit_value(path, valuetype, callback, initial, size) { let [keypadid, xcoord, ycoord] = keypads[valuetype]; - console.log('XXX TODO : Edit value', path, valuetype, callback, initial, keypadid); edit_callback = callback; let widget = hmi_widgets[keypadid]; widget.start_edit(path, valuetype, callback, initial, size); @@ -461,7 +459,6 @@ eltsub.inactive.setAttribute("style", "display:none"); if(eltsub.active_style !== undefined) eltsub.active.setAttribute("style", eltsub.active_style); - console.log("active", eltsub); }; function widget_inactive_activable(eltsub) { if(eltsub.active_style === undefined) @@ -469,5 +466,4 @@ eltsub.active.setAttribute("style", "display:none"); if(eltsub.inactive_style !== undefined) eltsub.inactive.setAttribute("style", eltsub.inactive_style); - console.log("inactive", eltsub); -}; +}; diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/svghmi.py --- a/svghmi/svghmi.py Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/svghmi.py Thu Sep 17 11:30:22 2020 +0200 @@ -216,7 +216,8 @@ hmi_types_instances.pop(i) break - assert(hmi_tree_root is not None) + if hmi_tree_root is None: + self.FatalError("SVGHMI : Library is selected but not used. Please either deselect it in project config or add a SVGHMI node to project.") # deduce HMI tree from PLC HMI_* instances for v in hmi_types_instances: @@ -484,11 +485,18 @@ InkscapeGeomColumns = ["Id", "x", "y", "w", "h"] inkpath = get_inkscape_path() + + if inkpath is None: + self.FatalError("SVGHMI: inkscape is not installed.") + svgpath = self._getSVGpath() - _status, result, _err_result = ProcessLogger(self.GetCTRoot().logger, - inkpath + " -S " + svgpath, + status, result, _err_result = ProcessLogger(self.GetCTRoot().logger, + '"' + inkpath + '" -S "' + svgpath + '"', no_stdout=True, no_stderr=True).spin() + if status != 0: + self.FatalError("SVGHMI: inkscape couldn't extract geometry from given SVG.") + res = [] for line in result.split(): strippedline = line.strip() diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/widget_display.ysl2 --- a/svghmi/widget_display.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/widget_display.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -7,7 +7,6 @@ frequency = 5; dispatch(value, oldval, index) { this.fields[index] = value; - console.log(value, index); this.element.textContent = this.args.length == 1 ? vsprintf(this.args[0],this.fields) : this.fields.join(' '); } } diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/widget_dropdown.ysl2 --- a/svghmi/widget_dropdown.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/widget_dropdown.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -158,7 +158,6 @@ 0, this.menu_offset - spanslength); } - console.log(this.menu_offset); this.set_partial_text(); }, // Setup partial view text content diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/widget_input.ysl2 --- a/svghmi/widget_input.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/widget_input.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -35,7 +35,7 @@ | }, | on_edit_click: function(opstr) { | var size = (typeof this.key_pos_elt !== 'undefined') ? this.key_pos_elt.getBBox() : undefined - | edit_value("«path/@value»", "«path/@type»", this, this.last_val,size); + | edit_value("«path/@value»", "«path/@type»", this, this.last_val, size); | }, | edit_callback: function(new_val) { diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/widget_jsontable.ysl2 --- a/svghmi/widget_jsontable.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/widget_jsontable.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -3,32 +3,32 @@ template "widget[@type='JsonTable']", mode="widget_class" || class JsonTableWidget extends Widget{ - do_http_request() { + cache = []; + do_http_request(...opt) { const query = { - offset: '42', - filter: '*powerloss*', - args: this.args + args: this.args, + vars: this.cache, + visible: this.visible, + options: opt }; const options = { method: 'POST', body: JSON.stringify(query), headers: {'Content-Type': 'application/json'} - } + }; fetch(this.args[0], options) .then(res => res.json()) - .then(this.spread_json_data); - - } - dispatch(value) { + .then(this.spread_json_data.bind(this)); + + } + dispatch(value, oldval, index) { + this.cache[index] = value; this.do_http_request(); } - on_click(evt) { - this.do_http_request(); - } - init() { - this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + on_click(evt, ...options) { + this.do_http_request(...options); } } || @@ -41,44 +41,171 @@ const "hmi_textstylelists_descs", "$parsed_widgets/widget[@type = 'TextStyleList']"; const "hmi_textstylelists", "$hmi_elements[@id = $hmi_textstylelists_descs/@id]"; +const "textstylelist_related" foreach "$hmi_textstylelists" list { + attrib "listid" value "@id"; + foreach "func:refered_elements(.)" elt { + attrib "eltid" value "@id"; + } +} +const "textstylelist_related_ns", "exsl:node-set($textstylelist_related)"; + +def "func:json_expressions" { + param "expressions"; + param "label"; + + // compute javascript expressions to access JSON data + // desscribed in given svg element's "label" + // knowing that parent element already has given "expressions". + + choose { + when "$label" { + const "suffixes", "str:split($label)"; + const "res" foreach "$suffixes" expression { + const "suffix","."; + const "pos","position()"; + // take last available expression (i.e can have more suffixes than expressions) + const "expr","$expressions[position() <= $pos][last()]/expression"; + choose { + when "contains($suffix,'=')" { + const "name", "substring-before($suffix,'=')"; + if "$expr/@name[. != $name]" + error > JsonTable : missplaced '=' or inconsistent names in Json data expressions. + attrib "name" value "$name"; + attrib "content" > «$expr/@content»«substring-after($suffix,'=')» + } + otherwise { + copy "$expr/@name"; + attrib "content" > «$expr/@content»«$suffix» + } + } + } + result "exsl:node-set($res)"; + } + // Empty labels are ignored, expressions are then passed as-is. + otherwise result "$expressions"; + } + +} + +const "initexpr" expression attrib "content" > jdata +const "initexpr_ns", "exsl:node-set($initexpr)"; + template "svg:use", mode="json_table_elt_render" { - param "value_expr"; + param "expressions"; // cloned element must be part of a HMI:List const "targetid", "substring-after(@xlink:href,'#')"; const "from_list", "$hmi_lists[(@id | */@id) = $targetid]"; - const "from_textstylelist", "$hmi_textstylelists[(@id | */@id) = $targetid]"; choose { when "count($from_list) > 0" { | id("«@id»").setAttribute("xlink:href", // obtain new target id from HMI:List widget - | "#"+hmi_widgets["«$from_list/@id»"].items[«$value_expr»]); - } + | "#"+hmi_widgets["«$from_list/@id»"].items[«$expressions/expression[1]/@content»]); + } + otherwise + warning > Clones (svg:use) in JsonTable Widget must point to a valid HMI:List widget or item. Reference "«@xlink:href»" is not valid and will not be updated. + } +} + +template "svg:text", mode="json_table_elt_render" { + param "expressions"; + const "value_expr", "$expressions/expression[1]/@content"; + const "original", "@original"; + const "from_textstylelist", "$textstylelist_related_ns/list[elt/@eltid = $original]"; + choose { + when "count($from_textstylelist) > 0" { - | console.log("from_textsylelist","«@id»", "«$value_expr»", «$value_expr», - // obtain new style from HMI:TextStyleList widget - | hmi_widgets["«$from_textstylelist/@id»"].items[«$value_expr»]); - } - otherwise - warning > Clones (svg:use) in JsonTable Widget must point to a valid HMI:List or HMI:TextStyleList widget or item. Reference "«@xlink:href»" is not valid and will not be updated. - } -} - -template "svg:text", mode="json_table_elt_render" { - param "value_expr"; - | id("«@id»").textContent = String(«$value_expr»); -} + const "content_expr", "$expressions/expression[2]/@content"; + if "string-length($content_expr) = 0 or $expressions/expression[2]/@name != 'textContent'" + error > Clones (svg:use) in JsonTable Widget pointing to a HMI:TextStyleList widget or item must have a "textContent=.someVal" assignement following value expression in label. + | { + | let elt = id("«@id»"); + | elt.textContent = String(«$content_expr»); + | elt.style = hmi_widgets["«$from_textstylelist/@listid»"].styles[«$value_expr»]; + | } + } + otherwise { + | id("«@id»").textContent = String(«$value_expr»); + } + } +} + + +// only labels comming from Json widget are counted in +def "func:filter_non_widget_label" { + param "elt"; + param "widget_elts"; + const "eltid" choose { + when "$elt/@original" value "$elt/@original"; + otherwise value "$elt/@id"; + } + result "$widget_elts[@id=$eltid]/@inkscape:label"; +} + +template "svg:*", mode="json_table_render_except_comments"{ + param "expressions"; + param "widget_elts"; + + const "label", "func:filter_non_widget_label(., $widget_elts)"; + // filter out "# commented" elements + if "not(starts-with($label,'#'))" + apply ".", mode="json_table_render"{ + with "expressions", "$expressions"; + with "widget_elts", "$widget_elts"; + with "label", "$label"; + } +} + template "svg:*", mode="json_table_render" { - param "objname"; - apply ".", mode="json_table_elt_render" with "value_expr" > «$objname»«substring-before(@inkscape:label, ' ')» + param "expressions"; + param "widget_elts"; + param "label"; + + const "new_expressions", "func:json_expressions($expressions, $label)"; + + const "elt","."; + foreach "$new_expressions/expression[position() > 1][starts-with(@name,'onClick')]" + | id("«$elt/@id»").setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt, '«@name»', '"+«@content»+"')"); + + apply ".", mode="json_table_elt_render" + with "expressions", "$new_expressions"; } template "svg:g", mode="json_table_render" { - param "objname"; - | let obj_«@id» = «$objname»«substring-before(@inkscape:label, ' ')»; - apply "*[@inkscape:label]", mode="json_table_render" - with "objname" > obj_«@id» + param "expressions"; + param "widget_elts"; + param "label"; + const "gid", "@id"; + + // use intermediate variables for optimization + const "varprefix" > obj_«$gid»_ + | try { + + foreach "$expressions/expression"{ + | let «$varprefix»«position()» = «@content»; + | if(«$varprefix»«position()» == undefined) { + | console.log("«$varprefix»«position()» = «@content»"); + | throw null; + | } + } + + // because we put values in a variables, we can replace corresponding expression with variable name + const "new_expressions" foreach "$expressions/expression" xsl:copy { + copy "@name"; + attrib "content" > «$varprefix»«position()» + } + + // revert hiding in case it did happen before + | id("«@id»").setAttribute("style", "«@style»"); + + apply "*", mode="json_table_render_except_comments" { + with "expressions", "func:json_expressions(exsl:node-set($new_expressions), $label)"; + with "widget_elts", "$widget_elts"; + } + | } catch(err) { + | id("«$gid»").setAttribute("style", "display:none"); + | } } template "widget[@type='JsonTable']", mode="widget_defs" { @@ -86,7 +213,15 @@ labels("data"); optional_labels("forward backward cursor"); const "data_elt", "$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']"; - | spread_json_data: function(jdata) { - apply "$data_elt/*", mode="json_table_render" with "objname","'jdata'"; + | visible: «count($data_elt/*[@inkscape:label])», + | spread_json_data: function(janswer) { + | let [range,position,jdata] = janswer; + | this.apply_hmi_value(1, range); + | this.apply_hmi_value(2, position); + | console.log(range,position,jdata); + apply "$data_elt", mode="json_table_render_except_comments" { + with "expressions","$initexpr_ns"; + with "widget_elts","$hmi_element/*[@inkscape:label = 'data']/descendant::svg:*"; + } | } } diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/widget_keypad.ysl2 --- a/svghmi/widget_keypad.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/widget_keypad.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -104,9 +104,16 @@ } on_Enter_click() { - end_modal.call(this); - let callback_obj = this.result_callback_obj; - callback_obj.edit_callback(this.editstr); + let coercedval = (typeof this.initial) == "number" ? Number(this.editstr) : this.editstr; + if(typeof coercedval == 'number' && isNaN(coercedval)){ + // revert to initial so it explicitely shows input was ignored + this.editstr = String(this.initial); + this.update(); + } else { + let callback_obj = this.result_callback_obj; + end_modal.call(this); + callback_obj.edit_callback(coercedval); + } } on_BackSpace_click() { @@ -153,11 +160,13 @@ result_callback_obj = undefined; start_edit(info, valuetype, callback_obj, initial,size) { show_modal.call(this,size); - this.editstr = initial; + this.editstr = String(initial); this.result_callback_obj = callback_obj; this.Info_elt.textContent = info; this.shift = false; this.caps = false; + this.initial = initial; + this.update(); } diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/widget_list.ysl2 --- a/svghmi/widget_list.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/widget_list.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -1,10 +1,20 @@ // widget_list.ysl2 -template "widget[@type='List' or @type='TextStyleList']", mode="widget_defs" { +template "widget[@type='List']", mode="widget_defs" { param "hmi_element"; | items: { foreach "$hmi_element/*[@inkscape:label]" { - | «func:escape_quotes(@inkscape:label)»: "«@id»", + | «@inkscape:label»: "«@id»", } | }, } + +template "widget[@type='TextStyleList']", mode="widget_defs" { + param "hmi_element"; + | styles: { + foreach "$hmi_element/*[@inkscape:label]" { + const "style", "func:refered_elements(.)[self::svg:text]/@style"; + | «@inkscape:label»: "«$style»", + } + | }, +} diff -r 696301e869d5 -r 6ea4b7e1a9ed svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Wed Sep 16 09:42:26 2020 +0200 +++ b/svghmi/widgets_common.ysl2 Thu Sep 17 11:30:22 2020 +0200 @@ -29,8 +29,10 @@ choose { when "not(@index)" { choose { - when "not(@type)" + when "not(@type)" { warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree + > undefined`if "position()!=last()" > ,` + } when "@type = 'PAGE_LOCAL'" > "«@value»"`if "position()!=last()" > ,` when "@type = 'HMI_LOCAL'" @@ -152,6 +154,7 @@ if(!this.unsubscribable) for(let i = 0; i < this.indexes.length; i++) { let index = this.get_variable_index(i); + if(index == undefined) continue; subscribers(index).add(this); } need_cache_apply.push(this); @@ -161,6 +164,7 @@ if(!this.unsubscribable) for(let index in this.indexes){ /* dispatch current cache in newly opened page widgets */ let realindex = this.get_variable_index(index); + if(realindex == undefined) continue; let cached_val = cache[realindex]; if(cached_val != undefined) this._dispatch(cached_val, cached_val, index); @@ -178,18 +182,23 @@ } return index; } - change_hmi_value(index,opstr) { - return change_hmi_value(this.get_variable_index(index), opstr); + change_hmi_value(index, opstr) { + let realindex = this.get_variable_index(index); + if(realindex == undefined) return undefined; + return change_hmi_value(realindex, opstr); } apply_hmi_value(index, new_val) { - return apply_hmi_value(this.get_variable_index(0), new_val); + let realindex = this.get_variable_index(index); + if(realindex == undefined) return undefined; + return apply_hmi_value(realindex, new_val); } new_hmi_value(index, value, oldval) { // TODO avoid searching, store index at sub() for(let i = 0; i < this.indexes.length; i++) { let refindex = this.get_variable_index(i); + if(refindex == undefined) continue; if(index == refindex) { this._dispatch(value, oldval, i); diff -r 696301e869d5 -r 6ea4b7e1a9ed tests/svghmi/py_ext_0@py_ext/pyfile.xml --- a/tests/svghmi/py_ext_0@py_ext/pyfile.xml Wed Sep 16 09:42:26 2020 +0200 +++ b/tests/svghmi/py_ext_0@py_ext/pyfile.xml Thu Sep 17 11:30:22 2020 +0200 @@ -1,33 +1,69 @@ - - + + + + delta else old_position + new_visible = new_range if delta <= 0 else visible + + visible_alarms = [] + for ts, text, status, alarmid in Alarms[new_position:new_position + new_visible]: + visible_alarms.append({ + "time": time.ctime(ts), + "text": text, # TODO translate text + "status": status, + "alarmid": alarmid + }) + + return new_range, new_position, visible_alarms + ]]> diff -r 696301e869d5 -r 6ea4b7e1a9ed tests/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg Wed Sep 16 09:42:26 2020 +0200 +++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Thu Sep 17 11:30:22 2020 +0200 @@ -16,7 +16,7 @@ version="1.1" id="hmi0" sodipodi:docname="svghmi.svg" - inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> + inkscape:version="0.92.3 (2405546, 2018-03-11)"> @@ -34,6 +34,21 @@ + + + @@ -182,17 +197,17 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:document-units="px" - inkscape:current-layer="g907" + inkscape:current-layer="g1384" showgrid="false" units="px" - inkscape:zoom="0.50000001" - inkscape:cx="632.83299" - inkscape:cy="-317.59408" - inkscape:window-width="2443" - inkscape:window-height="1567" - inkscape:window-x="816" - inkscape:window-y="103" - inkscape:window-maximized="0" + inkscape:zoom="1.0913159" + inkscape:cx="-911.00114" + inkscape:cy="181.96708" + inkscape:window-width="1800" + inkscape:window-height="836" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" /> + inkscape:label="HMI:Keypad:HMI_STRING:HMI_LOCAL:PAGE_LOCAL"> - 8888 - - - - dhu - - - - plop - - - - mhoo - - - - yodl - - - - mhe - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - information - - 8888 - - - - + style="fill:none;fill-rule:evenodd;stroke:#ff3000;stroke-width:2.9633333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:29.63333301;stroke-opacity:1;marker-end:url(#marker1656)" /> 8888 + transform="matrix(0.14295135,0,0,0.14295135,489.21833,37.615184)"> 8888 + style="stroke-width:4px">8888 - + @@ -4269,21 +3786,21 @@ id="path1465" d="m 797.19546,145.18619 -80.62929,0.60214 -0.60215,-80.629288 80.6293,-0.60214 z" inkscape:transform-center-y="-14.956361" - style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> dhu @@ -4302,12 +3819,12 @@ sodipodi:cx="596.74072" sodipodi:sides="3" id="path1473" - style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" sodipodi:type="star" transform="matrix(0,-2.0000001,1.9999999,0,1034.195,1298.6541)" /> plop - - plop + + @@ -4326,7 +3843,7 @@ inkscape:transform-center-x="14.956364" transform="rotate(-90,746.45698,-44.543641)" sodipodi:type="star" - style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" id="path1481" sodipodi:sides="3" sodipodi:cx="596.74072" @@ -4344,16 +3861,16 @@ id="text1485" y="111.05016" x="537.25018" - style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" xml:space="preserve">mhoo @@ -4373,22 +3890,22 @@ sodipodi:cx="596.74072" sodipodi:sides="3" id="path1489" - style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" sodipodi:type="star" /> yodl @@ -4396,7 +3913,7 @@ inkscape:transform-center-x="-14.956349" transform="matrix(0,-2.0000001,-1.9999999,0,1122.1514,1298.6541)" sodipodi:type="star" - style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:20;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" id="path1497" sodipodi:sides="3" sodipodi:cx="596.74072" @@ -4412,7 +3929,7 @@ inkscape:transform-center-y="-3.3040441e-05" /> mhe + style="stroke-width:2px">mhe HMI_LOCAL variables - - - - - + x="509.67926" + y="43.42762">HMI_LOCAL variables 8888 + style="stroke-width:4"> 8888 - + dhu + style="stroke-width:2px">dhu + style="stroke-width:4"> + style="stroke-width:4"> mhoo + style="stroke-width:2px">mhoo + style="stroke-width:4"> yodl + style="stroke-width:2px">yodl + style="stroke-width:4"> @@ -4656,9 +4145,9 @@ id="text1601" y="111.05016" x="842.71497" - style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + style="font-style:normal;font-weight:normal;font-size:20px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" xml:space="preserve"> PAGE_LOCAL variables @@ -4700,7 +4189,7 @@ inkscape:connector-curvature="0" id="path691" d="M 130.96206,4.0725977 79.111776,-41.363223" - style="fill:none;fill-rule:evenodd;stroke:#c130f7;stroke-width:2.96323133;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0, 32.59554547;stroke-dashoffset:29.63231468;stroke-opacity:1;marker-end:url(#marker1536)" /> + style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ff3000;stroke-width:2.96333337;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0, 32.59666667;stroke-dashoffset:29.63333321;stroke-opacity:1;marker-end:url(#marker1971)" /> + + + Alarm Page + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8888 + + + + + + + + + + + transform="translate(-1566.6506,56.936266)"> + transform="translate(-990.65059,102.93627)"> + + 8888 + + + + + + 8888 + + + + + range + position + notify + + + 8888 + + + + + + + + trigger + + + + + 8888 + + + ack + + + + disabled + + + + active + + + + alarm + + + Alarm Text + Status + TODO + + + + + + Alarms + + diff -r 696301e869d5 -r 6ea4b7e1a9ed tests/svghmi_v2/svghmi_0@svghmi/svghmi.svg --- a/tests/svghmi_v2/svghmi_0@svghmi/svghmi.svg Wed Sep 16 09:42:26 2020 +0200 +++ b/tests/svghmi_v2/svghmi_0@svghmi/svghmi.svg Thu Sep 17 11:30:22 2020 +0200 @@ -53,17 +53,6 @@ inkscape:vp_z="1272 : 385 : 1" inkscape:persp3d-origin="536 : 237 : 1" id="perspective445" /> - - - - @@ -91,26 +80,6 @@ id="path924" inkscape:connector-curvature="0" /> - - - - - - -