Edouard@2783: // svghmi.js
Edouard@2783: 
Edouard@3080: var need_cache_apply = [];
Edouard@2803: 
Edouard@2798: function dispatch_value(index, value) {
Edouard@3022:     let widgets = subscribers(index);
Edouard@2800: 
Edouard@2805:     let oldval = cache[index];
Edouard@2805:     cache[index] = value;
Edouard@2805: 
Edouard@2800:     if(widgets.size > 0) {
Edouard@2800:         for(let widget of widgets){
edouard@3006:             widget.new_hmi_value(index, value, oldval);
Edouard@2800:         }
Edouard@2800:     }
Edouard@2798: };
Edouard@2783: 
Edouard@2801: function init_widgets() {
Edouard@2801:     Object.keys(hmi_widgets).forEach(function(id) {
Edouard@2801:         let widget = hmi_widgets[id];
Edouard@2801:         let init = widget.init;
Edouard@2801:         if(typeof(init) == "function"){
Edouard@2834:             try {
Edouard@2834:                 init.call(widget);
Edouard@2834:             } catch(err) {
Edouard@2836:                 console.log(err);
Edouard@2834:             }
Edouard@2801:         }
Edouard@2801:     });
Edouard@2801: };
Edouard@2801: 
Edouard@2798: // Open WebSocket to relative "/ws" address
edouard@3268: 
edouard@3268: var ws_url = 
edouard@3268:     window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws')
edouard@3268:     + '?mode=' + (window.location.hash == "#watchdog" 
edouard@3268:                   ? "watchdog"
edouard@3268:                   : "multiclient");
edouard@3268: var ws = new WebSocket(ws_url);
Edouard@2798: ws.binaryType = 'arraybuffer';
Edouard@2783: 
Edouard@2798: const dvgetters = {
edouard@2826:     INT: (dv,offset) => [dv.getInt16(offset, true), 2],
edouard@2826:     BOOL: (dv,offset) => [dv.getInt8(offset, true), 1],
edouard@2890:     NODE: (dv,offset) => [dv.getInt8(offset, true), 1],
edouard@3068:     REAL: (dv,offset) => [dv.getFloat32(offset, true), 4],
edouard@2826:     STRING: (dv, offset) => {
Edouard@3080:         const size = dv.getInt8(offset);
edouard@2826:         return [
edouard@2826:             String.fromCharCode.apply(null, new Uint8Array(
edouard@2826:                 dv.buffer, /* original buffer */
edouard@2826:                 offset + 1, /* string starts after size*/
edouard@2826:                 size /* size of string */
edouard@2826:             )), size + 1]; /* total increment */
edouard@2826:     }
Edouard@2798: };
Edouard@2798: 
edouard@2859: // Apply updates recieved through ws.onmessage to subscribed widgets
Edouard@2865: function apply_updates() {
edouard@3152:     updates.forEach((value, index) => {
edouard@3152:         dispatch_value(index, value);
edouard@3152:     });
edouard@3152:     updates.clear();
Edouard@2865: }
Edouard@2865: 
Edouard@2865: // Called on requestAnimationFrame, modifies DOM
Edouard@2865: var requestAnimationFrameID = null;
Edouard@2865: function animate() {
Edouard@2865:     // Do the page swith if any one pending
Edouard@2865:     if(current_subscribed_page != current_visible_page){
Edouard@2865:         switch_visible_page(current_subscribed_page);
Edouard@2865:     }
Edouard@2895: 
Edouard@2897:     while(widget = need_cache_apply.pop()){
Edouard@2897:         widget.apply_cache();
Edouard@2897:     }
Edouard@2897: 
Edouard@2903:     if(jumps_need_update) update_jumps();
Edouard@2903: 
Edouard@2865:     apply_updates();
Edouard@3019: 
Edouard@3019:     pending_widget_animates.forEach(widget => widget._animate());
Edouard@3019:     pending_widget_animates = [];
Edouard@3019: 
Edouard@2864:     requestAnimationFrameID = null;
edouard@2859: }
edouard@2859: 
Edouard@2860: function requestHMIAnimation() {
Edouard@2864:     if(requestAnimationFrameID == null){
Edouard@2864:         requestAnimationFrameID = window.requestAnimationFrame(animate);
Edouard@2864:     }
Edouard@2860: }
Edouard@2860: 
edouard@2859: // Message reception handler
edouard@2859: // Hash is verified and HMI values updates resulting from binary parsing
edouard@2859: // are stored until browser can compute next frame, DOM is left untouched
Edouard@2798: ws.onmessage = function (evt) {
Edouard@2798: 
Edouard@2798:     let data = evt.data;
Edouard@2798:     let dv = new DataView(data);
Edouard@2798:     let i = 0;
Edouard@2799:     try {
Edouard@2799:         for(let hash_int of hmi_hash) {
Edouard@2799:             if(hash_int != dv.getUint8(i)){
Edouard@2800:                 throw new Error("Hash doesn't match");
Edouard@2799:             };
Edouard@2799:             i++;
Edouard@2799:         };
Edouard@2798: 
Edouard@2799:         while(i < data.byteLength){
Edouard@2799:             let index = dv.getUint32(i, true);
Edouard@2799:             i += 4;
Edouard@2799:             let iectype = hmitree_types[index];
Edouard@2799:             if(iectype != undefined){
edouard@2826:                 let dvgetter = dvgetters[iectype];
edouard@2826:                 let [value, bytesize] = dvgetter(dv,i);
edouard@3152:                 updates.set(index, value);
Edouard@2799:                 i += bytesize;
Edouard@2799:             } else {
edouard@2859:                 throw new Error("Unknown index "+index);
Edouard@2799:             }
Edouard@2799:         };
edouard@2859:         // register for rendering on next frame, since there are updates
Edouard@2864:         requestHMIAnimation();
Edouard@2799:     } catch(err) {
Edouard@2799:         // 1003 is for "Unsupported Data"
Edouard@2799:         // ws.close(1003, err.message);
Edouard@2798: 
Edouard@2799:         // TODO : remove debug alert ?
Edouard@2799:         alert("Error : "+err.message+"\\\\nHMI will be reloaded.");
Edouard@2798: 
Edouard@2799:         // force reload ignoring cache
Edouard@2799:         location.reload(true);
Edouard@2799:     }
Edouard@2798: };
Edouard@2783: 
Edouard@3080: hmi_hash_u8 = new Uint8Array(hmi_hash);
Edouard@2788: 
Edouard@2798: function send_blob(data) {
Edouard@2798:     if(data.length > 0) {
Edouard@3080:         ws.send(new Blob([hmi_hash_u8].concat(data)));
Edouard@2780:     };
Edouard@2798: };
Edouard@2798: 
Edouard@2798: const typedarray_types = {
Edouard@2829:     INT: (number) => new Int16Array([number]),
Edouard@2829:     BOOL: (truth) => new Int16Array([truth]),
edouard@2890:     NODE: (truth) => new Int16Array([truth]),
Edouard@3145:     REAL: (number) => new Float32Array([number]),
Edouard@2829:     STRING: (str) => {
Edouard@2829:         // beremiz default string max size is 128
Edouard@2829:         str = str.slice(0,128);
Edouard@2829:         binary = new Uint8Array(str.length + 1);
Edouard@2829:         binary[0] = str.length;
Edouard@3080:         for(let i = 0; i < str.length; i++){
Edouard@2829:             binary[i+1] = str.charCodeAt(i);
Edouard@2829:         }
Edouard@2829:         return binary;
Edouard@2829:     }
Edouard@2798:     /* TODO */
Edouard@2798: };
Edouard@2798: 
Edouard@2798: function send_reset() {
Edouard@2798:     send_blob(new Uint8Array([1])); /* reset = 1 */
Edouard@2798: };
Edouard@2798: 
Edouard@3022: var subscriptions = [];
Edouard@3022: 
Edouard@3022: function subscribers(index) {
Edouard@3022:     let entry = subscriptions[index];
Edouard@3022:     let res;
Edouard@3022:     if(entry == undefined){
Edouard@3022:         res = new Set();
Edouard@3022:         subscriptions[index] = [res,0];
Edouard@3022:     }else{
Edouard@3022:         [res, _ign] = entry;
Edouard@3022:     }
Edouard@3022:     return res
Edouard@3022: }
Edouard@3022: 
Edouard@3022: function get_subscription_period(index) {
Edouard@3022:     let entry = subscriptions[index];
Edouard@3022:     if(entry == undefined)
Edouard@3022:         return 0;
Edouard@3022:     let [_ign, period] = entry;
Edouard@3022:     return period;
Edouard@3022: }
Edouard@3022: 
Edouard@3022: function set_subscription_period(index, period) {
Edouard@3022:     let entry = subscriptions[index];
Edouard@3022:     if(entry == undefined){
Edouard@3022:         subscriptions[index] = [new Set(), period];
Edouard@3022:     } else {
Edouard@3022:         entry[1] = period;
Edouard@3022:     }
Edouard@3022: }
Edouard@2798: 
Edouard@2822: // artificially subscribe the watchdog widget to "/heartbeat" hmi variable
Edouard@2829: // Since dispatch directly calls change_hmi_value,
Edouard@2822: // PLC will periodically send variable at given frequency
Edouard@3022: subscribers(heartbeat_index).add({
Edouard@2822:     /* type: "Watchdog", */
Edouard@2822:     frequency: 1,
Edouard@2822:     indexes: [heartbeat_index],
edouard@3006:     new_hmi_value: function(index, value, oldval) {
edouard@3000:         apply_hmi_value(heartbeat_index, value+1);
Edouard@2822:     }
Edouard@2822: });
Edouard@2822: 
edouard@3142: function svg_text_to_multiline(elt) {
edouard@3142:     return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\\\\n")); 
edouard@3142: }
edouard@3142: 
edouard@3142: function multiline_to_svg_text(elt, str) {
edouard@3142:     str.split('\\\\n').map((line,i) => {elt.children[i].textContent = line;});
edouard@3142: }
edouard@3129: 
edouard@3129: function switch_langnum(langnum) {
edouard@3144:     langnum = Math.max(0, Math.min(langs.length - 1, langnum));
edouard@3129: 
edouard@3129:     for (let translation of translations) {
edouard@3144:         let [objs, msgs] = translation;
edouard@3144:         let msg = msgs[langnum];
edouard@3129:         for (let obj of objs) {
edouard@3142:             multiline_to_svg_text(obj, msg);
edouard@3142:             obj.setAttribute("lang",langnum);
edouard@3129:         }
edouard@3129:     }
edouard@3144:     return langnum;
edouard@3129: }
edouard@3142: 
edouard@3142: // backup original texts
edouard@3142: for (let translation of translations) {
edouard@3144:     let [objs, msgs] = translation;
edouard@3144:     msgs.unshift(svg_text_to_multiline(objs[0])); 
edouard@3142: }
edouard@3142: 
edouard@3129: var lang_local_index = hmi_local_index("lang");
edouard@3144: var langcode_local_index = hmi_local_index("lang_code");
edouard@3144: var langname_local_index = hmi_local_index("lang_name");
edouard@3129: subscribers(lang_local_index).add({
edouard@3129:     indexes: [lang_local_index],
edouard@3129:     new_hmi_value: function(index, value, oldval) {
edouard@3144:         let current_lang =  switch_langnum(value);
edouard@3144:         let [langname,langcode] = langs[current_lang];
edouard@3144:         apply_hmi_value(langcode_local_index, langcode);
edouard@3144:         apply_hmi_value(langname_local_index, langname);
edouard@3142:         switch_page();
edouard@3129:     }
edouard@3129: });
edouard@3144: 
edouard@3144: function setup_lang(){
edouard@3144:     let current_lang = cache[lang_local_index];
edouard@3144:     let new_lang = switch_langnum(current_lang);
edouard@3144:     if(current_lang != new_lang){
edouard@3144:         apply_hmi_value(lang_local_index, new_lang);
edouard@3144:     }
edouard@3144: }
edouard@3144: 
edouard@3144: setup_lang();
Edouard@3022: 
Edouard@2798: function update_subscriptions() {
Edouard@2798:     let delta = [];
Edouard@3022:     for(let index in subscriptions){
Edouard@3022:         let widgets = subscribers(index);
Edouard@2798: 
Edouard@2798:         // periods are in ms
Edouard@3022:         let previous_period = get_subscription_period(index);
Edouard@2798: 
Edouard@2822:         // subscribing with a zero period is unsubscribing
Edouard@2799:         let new_period = 0;
Edouard@2798:         if(widgets.size > 0) {
Edouard@2798:             let maxfreq = 0;
edouard@2960:             for(let widget of widgets){
edouard@2960:                 let wf = widget.frequency;
edouard@2960:                 if(wf != undefined && maxfreq < wf)
edouard@2960:                     maxfreq = wf;
edouard@2960:             }
Edouard@2798: 
Edouard@2799:             if(maxfreq != 0)
Edouard@2799:                 new_period = 1000/maxfreq;
Edouard@2798:         }
Edouard@2798: 
Edouard@2798:         if(previous_period != new_period) {
Edouard@3022:             set_subscription_period(index, new_period);
Edouard@3022:             if(index <= last_remote_index){
Edouard@3022:                 delta.push(
Edouard@3022:                     new Uint8Array([2]), /* subscribe = 2 */
Edouard@3022:                     new Uint32Array([index]),
Edouard@3022:                     new Uint16Array([new_period]));
Edouard@3022:             }
Edouard@2798:         }
Edouard@2798:     }
Edouard@2798:     send_blob(delta);
Edouard@2798: };
Edouard@2798: 
Edouard@2801: function send_hmi_value(index, value) {
edouard@3017:     if(index > last_remote_index){
edouard@3152:         updates.set(index, value);
edouard@3128: 
edouard@3128:         if(persistent_indexes.has(index)){
edouard@3128:             let varname = persistent_indexes.get(index);
edouard@3128:             document.cookie = varname+"="+value+"; max-age=3153600000";
edouard@3128:         }
edouard@3128: 
Edouard@3022:         requestHMIAnimation();
edouard@3017:         return;
edouard@3017:     }
edouard@3017: 
Edouard@2802:     let iectype = hmitree_types[index];
Edouard@2829:     let tobinary = typedarray_types[iectype];
Edouard@2798:     send_blob([
Edouard@2798:         new Uint8Array([0]),  /* setval = 0 */
Edouard@2829:         new Uint32Array([index]),
Edouard@2829:         tobinary(value)]);
Edouard@2798: 
Edouard@2921:     // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
Edouard@2921:     // cache[index] = value;
Edouard@2798: };
Edouard@2798: 
Edouard@2911: function apply_hmi_value(index, new_val) {
Edouard@3190:     let old_val = cache[index];
Edouard@2911:     if(new_val != undefined && old_val != new_val)
Edouard@2911:         send_hmi_value(index, new_val);
Edouard@2911:     return new_val;
Edouard@2911: }
Edouard@2911: 
Edouard@3078: const quotes = {"'":null, '"':null};
edouard@2970: 
edouard@3098: function eval_operation_string(old_val, opstr) {
Edouard@2801:     let op = opstr[0];
edouard@2970:     let given_val;
edouard@2970:     if(opstr.length < 2) 
edouard@3098:         return undefined;
edouard@2970:     if(opstr[1] in quotes){
edouard@2970:         if(opstr.length < 3) 
edouard@3098:             return undefined;
edouard@2970:         if(opstr[opstr.length-1] == opstr[1]){
edouard@2970:             given_val = opstr.slice(2,opstr.length-1);
edouard@2970:         }
edouard@2970:     } else {
edouard@2970:         given_val = Number(opstr.slice(1));
edouard@2970:     }
Edouard@2803:     let new_val;
Edouard@2803:     switch(op){
Edouard@2803:       case "=":
edouard@2970:         new_val = given_val;
Edouard@2803:         break;
Edouard@2803:       case "+":
edouard@2970:         new_val = old_val + given_val;
edouard@2970:         break;
Edouard@2803:       case "-":
edouard@2970:         new_val = old_val - given_val;
edouard@2970:         break;
Edouard@2803:       case "*":
edouard@2970:         new_val = old_val * given_val;
edouard@2970:         break;
Edouard@2803:       case "/":
edouard@2970:         new_val = old_val / given_val;
Edouard@2803:         break;
Edouard@2803:     }
edouard@3098:     return new_val;
edouard@3098: }
edouard@3098: 
Edouard@2864: var current_visible_page;
Edouard@2864: var current_subscribed_page;
Edouard@2903: var current_page_index;
Edouard@3206: var page_node_local_index = hmi_local_index("page_node");
Edouard@2798: 
Edouard@3299: function toggleFullscreen() {
Edouard@3299:   let elem = document.documentElement;
Edouard@3299: 
Edouard@3299:   if (!document.fullscreenElement) {
Edouard@3299:     elem.requestFullscreen().catch(err => {
Edouard@3299:       console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")");
Edouard@3299:     });
Edouard@3299:   } else {
Edouard@3299:     document.exitFullscreen();
Edouard@3299:   }
Edouard@3299: }
Edouard@3299: 
Edouard@2843: function prepare_svg() {
Edouard@3082:     // prevents context menu from appearing on right click and long touch
Edouard@3082:     document.body.addEventListener('contextmenu', e => {
Edouard@3299:         toggleFullscreen();
Edouard@3082:         e.preventDefault();
Edouard@3082:     });
Edouard@3082: 
edouard@2850:     for(let eltid in detachable_elements){
edouard@2850:         let [element,parent] = detachable_elements[eltid];
edouard@2850:         parent.removeChild(element);
edouard@2850:     }
Edouard@2843: };
Edouard@2843: 
Edouard@2870: function switch_page(page_name, page_index) {
Edouard@2864:     if(current_subscribed_page != current_visible_page){
Edouard@2864:         /* page switch already going */
Edouard@2864:         /* TODO LOG ERROR */
Edouard@2902:         return false;
Edouard@2895:     }
Edouard@2895: 
Edouard@2895:     if(page_name == undefined)
Edouard@2895:         page_name = current_subscribed_page;
Edouard@2895: 
Edouard@2903: 
Edouard@2903:     let old_desc = page_desc[current_subscribed_page];
Edouard@2903:     let new_desc = page_desc[page_name];
Edouard@2903: 
Edouard@2903:     if(new_desc == undefined){
Edouard@2903:         /* TODO LOG ERROR */
Edouard@2903:         return false;
Edouard@2903:     }
Edouard@2903: 
Edouard@2903:     if(page_index == undefined){
Edouard@2903:         page_index = new_desc.page_index;
Edouard@2903:     }
Edouard@2903: 
Edouard@2903:     if(old_desc){
edouard@3005:         old_desc.widgets.map(([widget,relativeness])=>widget.unsub());
edouard@3005:     }
Edouard@3080:     const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
Edouard@3080: 
Edouard@3080:     const container_id = page_name + (page_index != undefined ? page_index : "");
edouard@3017: 
edouard@3017:     new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id));
Edouard@2903: 
Edouard@2903:     update_subscriptions();
Edouard@2903: 
Edouard@2903:     current_subscribed_page = page_name;
Edouard@2903:     current_page_index = page_index;
Edouard@3206:     let page_node;
Edouard@3206:     if(page_index != undefined){
Edouard@3206:         page_node = hmitree_paths[page_index];
Edouard@3206:     }else{
Edouard@3206:         page_node = "";
Edouard@3206:     }
Edouard@3206:     apply_hmi_value(page_node_local_index, page_node);
Edouard@2903: 
Edouard@2903:     jumps_need_update = true;
Edouard@2903: 
Edouard@2903:     requestHMIAnimation();
Edouard@2903:     jump_history.push([page_name, page_index]);
Edouard@2905:     if(jump_history.length > 42)
Edouard@2905:         jump_history.shift();
Edouard@2903: 
Edouard@2903:     return true;
Edouard@2864: };
Edouard@2864: 
Edouard@2864: function switch_visible_page(page_name) {
Edouard@2864: 
Edouard@2864:     let old_desc = page_desc[current_visible_page];
Edouard@2864:     let new_desc = page_desc[page_name];
Edouard@2864: 
Edouard@2864:     if(old_desc){
edouard@2850:         for(let eltid in old_desc.required_detachables){
edouard@2850:             if(!(eltid in new_desc.required_detachables)){
edouard@2850:                 let [element, parent] = old_desc.required_detachables[eltid];
edouard@2850:                 parent.removeChild(element);
edouard@2850:             }
edouard@2850:         }
edouard@2850:         for(let eltid in new_desc.required_detachables){
edouard@2850:             if(!(eltid in old_desc.required_detachables)){
edouard@2850:                 let [element, parent] = new_desc.required_detachables[eltid];
edouard@2850:                 parent.appendChild(element);
edouard@2850:             }
edouard@2850:         }
edouard@2850:     }else{
edouard@2850:         for(let eltid in new_desc.required_detachables){
edouard@2850:             let [element, parent] = new_desc.required_detachables[eltid];
edouard@2850:             parent.appendChild(element);
edouard@2850:         }
Edouard@2843:     }
Edouard@2843: 
Edouard@2895:     svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
Edouard@2895:     current_visible_page = page_name;
Edouard@2895: };
Edouard@2798: 
Edouard@2798: // Once connection established
Edouard@2798: ws.onopen = function (evt) {
Edouard@2801:     init_widgets();
Edouard@2798:     send_reset();
Edouard@2798:     // show main page
Edouard@2843:     prepare_svg();
Edouard@2798:     switch_page(default_page);
Edouard@2799: };
Edouard@2799: 
Edouard@2799: ws.onclose = function (evt) {
Edouard@2799:     // TODO : add visible notification while waiting for reload
Edouard@2799:     console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s.");
Edouard@2799:     // TODO : re-enable auto reload when not in debug
Edouard@2799:     //window.setTimeout(() => location.reload(true), 10000);
Edouard@2799:     alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+".");
Edouard@2798: 
Edouard@2798: };
Edouard@2911: 
Edouard@3080: const xmlns = "http://www.w3.org/2000/svg";
Edouard@2911: var edit_callback;
Edouard@3075: const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null}
Edouard@3118: function edit_value(path, valuetype, callback, initial) {
Edouard@3075:     if(valuetype in localtypes){
Edouard@3075:         valuetype = (typeof initial) == "number" ? "HMI_REAL" : "HMI_STRING";
Edouard@3075:     }
Edouard@2913:     let [keypadid, xcoord, ycoord] = keypads[valuetype];
Edouard@2911:     edit_callback = callback;
Edouard@2917:     let widget = hmi_widgets[keypadid];
Edouard@3118:     widget.start_edit(path, valuetype, callback, initial);
Edouard@2917: };
Edouard@2917: 
Edouard@2917: var current_modal; /* TODO stack ?*/
Edouard@2917: 
Edouard@3118: function show_modal() {
Edouard@2917:     let [element, parent] = detachable_elements[this.element.id];
Edouard@2917: 
Edouard@2916:     tmpgrp = document.createElementNS(xmlns,"g");
Edouard@2913:     tmpgrpattr = document.createAttribute("transform");
Edouard@2917:     let [xcoord,ycoord] = this.coordinates;
Edouard@2913:     let [xdest,ydest] = page_desc[current_visible_page].bbox;
Edouard@3118:     tmpgrpattr.value = "translate("+String(xdest-xcoord)+","+String(ydest-ycoord)+")";
usveticic@3010: 
Edouard@2913:     tmpgrp.setAttributeNode(tmpgrpattr);
Edouard@2913: 
Edouard@2913:     tmpgrp.appendChild(element);
Edouard@2913:     parent.appendChild(tmpgrp);
Edouard@2913: 
Edouard@2917:     current_modal = [this.element.id, tmpgrp];
Edouard@2917: };
Edouard@2917: 
Edouard@2917: function end_modal() {
Edouard@2917:     let [eltid, tmpgrp] = current_modal;
Edouard@2917:     let [element, parent] = detachable_elements[this.element.id];
Edouard@2917: 
Edouard@2917:     parent.removeChild(tmpgrp);
Edouard@2917: 
Edouard@2917:     current_modal = undefined;
Edouard@2917: };
edouard@2920: