|
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 |