svghmi/svghmi.js
changeset 3302 c89fc366bebd
parent 3299 8b45d8494fae
child 3381 3a0908b0319d
equal deleted inserted replaced
2744:577118ebd179 3302:c89fc366bebd
       
     1 // svghmi.js
       
     2 
       
     3 var need_cache_apply = [];
       
     4 
       
     5 function dispatch_value(index, value) {
       
     6     let widgets = subscribers(index);
       
     7 
       
     8     let oldval = cache[index];
       
     9     cache[index] = value;
       
    10 
       
    11     if(widgets.size > 0) {
       
    12         for(let widget of widgets){
       
    13             widget.new_hmi_value(index, value, oldval);
       
    14         }
       
    15     }
       
    16 };
       
    17 
       
    18 function init_widgets() {
       
    19     Object.keys(hmi_widgets).forEach(function(id) {
       
    20         let widget = hmi_widgets[id];
       
    21         let init = widget.init;
       
    22         if(typeof(init) == "function"){
       
    23             try {
       
    24                 init.call(widget);
       
    25             } catch(err) {
       
    26                 console.log(err);
       
    27             }
       
    28         }
       
    29     });
       
    30 };
       
    31 
       
    32 // Open WebSocket to relative "/ws" address
       
    33 
       
    34 var ws_url = 
       
    35     window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws')
       
    36     + '?mode=' + (window.location.hash == "#watchdog" 
       
    37                   ? "watchdog"
       
    38                   : "multiclient");
       
    39 var ws = new WebSocket(ws_url);
       
    40 ws.binaryType = 'arraybuffer';
       
    41 
       
    42 const dvgetters = {
       
    43     INT: (dv,offset) => [dv.getInt16(offset, true), 2],
       
    44     BOOL: (dv,offset) => [dv.getInt8(offset, true), 1],
       
    45     NODE: (dv,offset) => [dv.getInt8(offset, true), 1],
       
    46     REAL: (dv,offset) => [dv.getFloat32(offset, true), 4],
       
    47     STRING: (dv, offset) => {
       
    48         const size = dv.getInt8(offset);
       
    49         return [
       
    50             String.fromCharCode.apply(null, new Uint8Array(
       
    51                 dv.buffer, /* original buffer */
       
    52                 offset + 1, /* string starts after size*/
       
    53                 size /* size of string */
       
    54             )), size + 1]; /* total increment */
       
    55     }
       
    56 };
       
    57 
       
    58 // Apply updates recieved through ws.onmessage to subscribed widgets
       
    59 function apply_updates() {
       
    60     updates.forEach((value, index) => {
       
    61         dispatch_value(index, value);
       
    62     });
       
    63     updates.clear();
       
    64 }
       
    65 
       
    66 // Called on requestAnimationFrame, modifies DOM
       
    67 var requestAnimationFrameID = null;
       
    68 function animate() {
       
    69     // Do the page swith if any one pending
       
    70     if(current_subscribed_page != current_visible_page){
       
    71         switch_visible_page(current_subscribed_page);
       
    72     }
       
    73 
       
    74     while(widget = need_cache_apply.pop()){
       
    75         widget.apply_cache();
       
    76     }
       
    77 
       
    78     if(jumps_need_update) update_jumps();
       
    79 
       
    80     apply_updates();
       
    81 
       
    82     pending_widget_animates.forEach(widget => widget._animate());
       
    83     pending_widget_animates = [];
       
    84 
       
    85     requestAnimationFrameID = null;
       
    86 }
       
    87 
       
    88 function requestHMIAnimation() {
       
    89     if(requestAnimationFrameID == null){
       
    90         requestAnimationFrameID = window.requestAnimationFrame(animate);
       
    91     }
       
    92 }
       
    93 
       
    94 // Message reception handler
       
    95 // Hash is verified and HMI values updates resulting from binary parsing
       
    96 // are stored until browser can compute next frame, DOM is left untouched
       
    97 ws.onmessage = function (evt) {
       
    98 
       
    99     let data = evt.data;
       
   100     let dv = new DataView(data);
       
   101     let i = 0;
       
   102     try {
       
   103         for(let hash_int of hmi_hash) {
       
   104             if(hash_int != dv.getUint8(i)){
       
   105                 throw new Error("Hash doesn't match");
       
   106             };
       
   107             i++;
       
   108         };
       
   109 
       
   110         while(i < data.byteLength){
       
   111             let index = dv.getUint32(i, true);
       
   112             i += 4;
       
   113             let iectype = hmitree_types[index];
       
   114             if(iectype != undefined){
       
   115                 let dvgetter = dvgetters[iectype];
       
   116                 let [value, bytesize] = dvgetter(dv,i);
       
   117                 updates.set(index, value);
       
   118                 i += bytesize;
       
   119             } else {
       
   120                 throw new Error("Unknown index "+index);
       
   121             }
       
   122         };
       
   123         // register for rendering on next frame, since there are updates
       
   124         requestHMIAnimation();
       
   125     } catch(err) {
       
   126         // 1003 is for "Unsupported Data"
       
   127         // ws.close(1003, err.message);
       
   128 
       
   129         // TODO : remove debug alert ?
       
   130         alert("Error : "+err.message+"\\\\nHMI will be reloaded.");
       
   131 
       
   132         // force reload ignoring cache
       
   133         location.reload(true);
       
   134     }
       
   135 };
       
   136 
       
   137 hmi_hash_u8 = new Uint8Array(hmi_hash);
       
   138 
       
   139 function send_blob(data) {
       
   140     if(data.length > 0) {
       
   141         ws.send(new Blob([hmi_hash_u8].concat(data)));
       
   142     };
       
   143 };
       
   144 
       
   145 const typedarray_types = {
       
   146     INT: (number) => new Int16Array([number]),
       
   147     BOOL: (truth) => new Int16Array([truth]),
       
   148     NODE: (truth) => new Int16Array([truth]),
       
   149     REAL: (number) => new Float32Array([number]),
       
   150     STRING: (str) => {
       
   151         // beremiz default string max size is 128
       
   152         str = str.slice(0,128);
       
   153         binary = new Uint8Array(str.length + 1);
       
   154         binary[0] = str.length;
       
   155         for(let i = 0; i < str.length; i++){
       
   156             binary[i+1] = str.charCodeAt(i);
       
   157         }
       
   158         return binary;
       
   159     }
       
   160     /* TODO */
       
   161 };
       
   162 
       
   163 function send_reset() {
       
   164     send_blob(new Uint8Array([1])); /* reset = 1 */
       
   165 };
       
   166 
       
   167 var subscriptions = [];
       
   168 
       
   169 function subscribers(index) {
       
   170     let entry = subscriptions[index];
       
   171     let res;
       
   172     if(entry == undefined){
       
   173         res = new Set();
       
   174         subscriptions[index] = [res,0];
       
   175     }else{
       
   176         [res, _ign] = entry;
       
   177     }
       
   178     return res
       
   179 }
       
   180 
       
   181 function get_subscription_period(index) {
       
   182     let entry = subscriptions[index];
       
   183     if(entry == undefined)
       
   184         return 0;
       
   185     let [_ign, period] = entry;
       
   186     return period;
       
   187 }
       
   188 
       
   189 function set_subscription_period(index, period) {
       
   190     let entry = subscriptions[index];
       
   191     if(entry == undefined){
       
   192         subscriptions[index] = [new Set(), period];
       
   193     } else {
       
   194         entry[1] = period;
       
   195     }
       
   196 }
       
   197 
       
   198 // artificially subscribe the watchdog widget to "/heartbeat" hmi variable
       
   199 // Since dispatch directly calls change_hmi_value,
       
   200 // PLC will periodically send variable at given frequency
       
   201 subscribers(heartbeat_index).add({
       
   202     /* type: "Watchdog", */
       
   203     frequency: 1,
       
   204     indexes: [heartbeat_index],
       
   205     new_hmi_value: function(index, value, oldval) {
       
   206         apply_hmi_value(heartbeat_index, value+1);
       
   207     }
       
   208 });
       
   209 
       
   210 function svg_text_to_multiline(elt) {
       
   211     return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\\\\n")); 
       
   212 }
       
   213 
       
   214 function multiline_to_svg_text(elt, str) {
       
   215     str.split('\\\\n').map((line,i) => {elt.children[i].textContent = line;});
       
   216 }
       
   217 
       
   218 function switch_langnum(langnum) {
       
   219     langnum = Math.max(0, Math.min(langs.length - 1, langnum));
       
   220 
       
   221     for (let translation of translations) {
       
   222         let [objs, msgs] = translation;
       
   223         let msg = msgs[langnum];
       
   224         for (let obj of objs) {
       
   225             multiline_to_svg_text(obj, msg);
       
   226             obj.setAttribute("lang",langnum);
       
   227         }
       
   228     }
       
   229     return langnum;
       
   230 }
       
   231 
       
   232 // backup original texts
       
   233 for (let translation of translations) {
       
   234     let [objs, msgs] = translation;
       
   235     msgs.unshift(svg_text_to_multiline(objs[0])); 
       
   236 }
       
   237 
       
   238 var lang_local_index = hmi_local_index("lang");
       
   239 var langcode_local_index = hmi_local_index("lang_code");
       
   240 var langname_local_index = hmi_local_index("lang_name");
       
   241 subscribers(lang_local_index).add({
       
   242     indexes: [lang_local_index],
       
   243     new_hmi_value: function(index, value, oldval) {
       
   244         let current_lang =  switch_langnum(value);
       
   245         let [langname,langcode] = langs[current_lang];
       
   246         apply_hmi_value(langcode_local_index, langcode);
       
   247         apply_hmi_value(langname_local_index, langname);
       
   248         switch_page();
       
   249     }
       
   250 });
       
   251 
       
   252 function setup_lang(){
       
   253     let current_lang = cache[lang_local_index];
       
   254     let new_lang = switch_langnum(current_lang);
       
   255     if(current_lang != new_lang){
       
   256         apply_hmi_value(lang_local_index, new_lang);
       
   257     }
       
   258 }
       
   259 
       
   260 setup_lang();
       
   261 
       
   262 function update_subscriptions() {
       
   263     let delta = [];
       
   264     for(let index in subscriptions){
       
   265         let widgets = subscribers(index);
       
   266 
       
   267         // periods are in ms
       
   268         let previous_period = get_subscription_period(index);
       
   269 
       
   270         // subscribing with a zero period is unsubscribing
       
   271         let new_period = 0;
       
   272         if(widgets.size > 0) {
       
   273             let maxfreq = 0;
       
   274             for(let widget of widgets){
       
   275                 let wf = widget.frequency;
       
   276                 if(wf != undefined && maxfreq < wf)
       
   277                     maxfreq = wf;
       
   278             }
       
   279 
       
   280             if(maxfreq != 0)
       
   281                 new_period = 1000/maxfreq;
       
   282         }
       
   283 
       
   284         if(previous_period != new_period) {
       
   285             set_subscription_period(index, new_period);
       
   286             if(index <= last_remote_index){
       
   287                 delta.push(
       
   288                     new Uint8Array([2]), /* subscribe = 2 */
       
   289                     new Uint32Array([index]),
       
   290                     new Uint16Array([new_period]));
       
   291             }
       
   292         }
       
   293     }
       
   294     send_blob(delta);
       
   295 };
       
   296 
       
   297 function send_hmi_value(index, value) {
       
   298     if(index > last_remote_index){
       
   299         updates.set(index, value);
       
   300 
       
   301         if(persistent_indexes.has(index)){
       
   302             let varname = persistent_indexes.get(index);
       
   303             document.cookie = varname+"="+value+"; max-age=3153600000";
       
   304         }
       
   305 
       
   306         requestHMIAnimation();
       
   307         return;
       
   308     }
       
   309 
       
   310     let iectype = hmitree_types[index];
       
   311     let tobinary = typedarray_types[iectype];
       
   312     send_blob([
       
   313         new Uint8Array([0]),  /* setval = 0 */
       
   314         new Uint32Array([index]),
       
   315         tobinary(value)]);
       
   316 
       
   317     // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf
       
   318     // cache[index] = value;
       
   319 };
       
   320 
       
   321 function apply_hmi_value(index, new_val) {
       
   322     let old_val = cache[index];
       
   323     if(new_val != undefined && old_val != new_val)
       
   324         send_hmi_value(index, new_val);
       
   325     return new_val;
       
   326 }
       
   327 
       
   328 const quotes = {"'":null, '"':null};
       
   329 
       
   330 function eval_operation_string(old_val, opstr) {
       
   331     let op = opstr[0];
       
   332     let given_val;
       
   333     if(opstr.length < 2) 
       
   334         return undefined;
       
   335     if(opstr[1] in quotes){
       
   336         if(opstr.length < 3) 
       
   337             return undefined;
       
   338         if(opstr[opstr.length-1] == opstr[1]){
       
   339             given_val = opstr.slice(2,opstr.length-1);
       
   340         }
       
   341     } else {
       
   342         given_val = Number(opstr.slice(1));
       
   343     }
       
   344     let new_val;
       
   345     switch(op){
       
   346       case "=":
       
   347         new_val = given_val;
       
   348         break;
       
   349       case "+":
       
   350         new_val = old_val + given_val;
       
   351         break;
       
   352       case "-":
       
   353         new_val = old_val - given_val;
       
   354         break;
       
   355       case "*":
       
   356         new_val = old_val * given_val;
       
   357         break;
       
   358       case "/":
       
   359         new_val = old_val / given_val;
       
   360         break;
       
   361     }
       
   362     return new_val;
       
   363 }
       
   364 
       
   365 var current_visible_page;
       
   366 var current_subscribed_page;
       
   367 var current_page_index;
       
   368 var page_node_local_index = hmi_local_index("page_node");
       
   369 
       
   370 function toggleFullscreen() {
       
   371   let elem = document.documentElement;
       
   372 
       
   373   if (!document.fullscreenElement) {
       
   374     elem.requestFullscreen().catch(err => {
       
   375       console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")");
       
   376     });
       
   377   } else {
       
   378     document.exitFullscreen();
       
   379   }
       
   380 }
       
   381 
       
   382 function prepare_svg() {
       
   383     // prevents context menu from appearing on right click and long touch
       
   384     document.body.addEventListener('contextmenu', e => {
       
   385         toggleFullscreen();
       
   386         e.preventDefault();
       
   387     });
       
   388 
       
   389     for(let eltid in detachable_elements){
       
   390         let [element,parent] = detachable_elements[eltid];
       
   391         parent.removeChild(element);
       
   392     }
       
   393 };
       
   394 
       
   395 function switch_page(page_name, page_index) {
       
   396     if(current_subscribed_page != current_visible_page){
       
   397         /* page switch already going */
       
   398         /* TODO LOG ERROR */
       
   399         return false;
       
   400     }
       
   401 
       
   402     if(page_name == undefined)
       
   403         page_name = current_subscribed_page;
       
   404 
       
   405 
       
   406     let old_desc = page_desc[current_subscribed_page];
       
   407     let new_desc = page_desc[page_name];
       
   408 
       
   409     if(new_desc == undefined){
       
   410         /* TODO LOG ERROR */
       
   411         return false;
       
   412     }
       
   413 
       
   414     if(page_index == undefined){
       
   415         page_index = new_desc.page_index;
       
   416     }
       
   417 
       
   418     if(old_desc){
       
   419         old_desc.widgets.map(([widget,relativeness])=>widget.unsub());
       
   420     }
       
   421     const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index;
       
   422 
       
   423     const container_id = page_name + (page_index != undefined ? page_index : "");
       
   424 
       
   425     new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id));
       
   426 
       
   427     update_subscriptions();
       
   428 
       
   429     current_subscribed_page = page_name;
       
   430     current_page_index = page_index;
       
   431     let page_node;
       
   432     if(page_index != undefined){
       
   433         page_node = hmitree_paths[page_index];
       
   434     }else{
       
   435         page_node = "";
       
   436     }
       
   437     apply_hmi_value(page_node_local_index, page_node);
       
   438 
       
   439     jumps_need_update = true;
       
   440 
       
   441     requestHMIAnimation();
       
   442     jump_history.push([page_name, page_index]);
       
   443     if(jump_history.length > 42)
       
   444         jump_history.shift();
       
   445 
       
   446     return true;
       
   447 };
       
   448 
       
   449 function switch_visible_page(page_name) {
       
   450 
       
   451     let old_desc = page_desc[current_visible_page];
       
   452     let new_desc = page_desc[page_name];
       
   453 
       
   454     if(old_desc){
       
   455         for(let eltid in old_desc.required_detachables){
       
   456             if(!(eltid in new_desc.required_detachables)){
       
   457                 let [element, parent] = old_desc.required_detachables[eltid];
       
   458                 parent.removeChild(element);
       
   459             }
       
   460         }
       
   461         for(let eltid in new_desc.required_detachables){
       
   462             if(!(eltid in old_desc.required_detachables)){
       
   463                 let [element, parent] = new_desc.required_detachables[eltid];
       
   464                 parent.appendChild(element);
       
   465             }
       
   466         }
       
   467     }else{
       
   468         for(let eltid in new_desc.required_detachables){
       
   469             let [element, parent] = new_desc.required_detachables[eltid];
       
   470             parent.appendChild(element);
       
   471         }
       
   472     }
       
   473 
       
   474     svg_root.setAttribute('viewBox',new_desc.bbox.join(" "));
       
   475     current_visible_page = page_name;
       
   476 };
       
   477 
       
   478 // Once connection established
       
   479 ws.onopen = function (evt) {
       
   480     init_widgets();
       
   481     send_reset();
       
   482     // show main page
       
   483     prepare_svg();
       
   484     switch_page(default_page);
       
   485 };
       
   486 
       
   487 ws.onclose = function (evt) {
       
   488     // TODO : add visible notification while waiting for reload
       
   489     console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s.");
       
   490     // TODO : re-enable auto reload when not in debug
       
   491     //window.setTimeout(() => location.reload(true), 10000);
       
   492     alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+".");
       
   493 
       
   494 };
       
   495 
       
   496 const xmlns = "http://www.w3.org/2000/svg";
       
   497 var edit_callback;
       
   498 const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null}
       
   499 function edit_value(path, valuetype, callback, initial) {
       
   500     if(valuetype in localtypes){
       
   501         valuetype = (typeof initial) == "number" ? "HMI_REAL" : "HMI_STRING";
       
   502     }
       
   503     let [keypadid, xcoord, ycoord] = keypads[valuetype];
       
   504     edit_callback = callback;
       
   505     let widget = hmi_widgets[keypadid];
       
   506     widget.start_edit(path, valuetype, callback, initial);
       
   507 };
       
   508 
       
   509 var current_modal; /* TODO stack ?*/
       
   510 
       
   511 function show_modal() {
       
   512     let [element, parent] = detachable_elements[this.element.id];
       
   513 
       
   514     tmpgrp = document.createElementNS(xmlns,"g");
       
   515     tmpgrpattr = document.createAttribute("transform");
       
   516     let [xcoord,ycoord] = this.coordinates;
       
   517     let [xdest,ydest] = page_desc[current_visible_page].bbox;
       
   518     tmpgrpattr.value = "translate("+String(xdest-xcoord)+","+String(ydest-ycoord)+")";
       
   519 
       
   520     tmpgrp.setAttributeNode(tmpgrpattr);
       
   521 
       
   522     tmpgrp.appendChild(element);
       
   523     parent.appendChild(tmpgrp);
       
   524 
       
   525     current_modal = [this.element.id, tmpgrp];
       
   526 };
       
   527 
       
   528 function end_modal() {
       
   529     let [eltid, tmpgrp] = current_modal;
       
   530     let [element, parent] = detachable_elements[this.element.id];
       
   531 
       
   532     parent.removeChild(tmpgrp);
       
   533 
       
   534     current_modal = undefined;
       
   535 };
       
   536