MIDAS
Loading...
Searching...
No Matches
mhttpd.js
Go to the documentation of this file.
1/********************************************************************\
2
3 Name: mhttpd.js
4 Created by: Stefan Ritt
5
6 Contents: JavaScript midas library used by mhttpd
7
8 Note: please load midas.js before loading mhttpd.js
9
10 \********************************************************************/
11
12let run_state_names = {1: "Stopped", 2: "Paused", 3: "Running"};
13
14let serverTimezoneOffset = undefined;
15
16let transition_names = {
17 1: "Starting run...",
18 2: "Stopping run...",
19 4: "Pausing run...",
20 8: "Resuming run...",
21 16: "Start abort",
22 4096: "Deferred"
23};
24
25// extend 2d canvas object
26CanvasRenderingContext2D.prototype.drawLine = function (x1, y1, x2, y2) {
27 this.beginPath();
28 this.moveTo(x1, y1);
29 this.lineTo(x2, y2);
30 this.stroke();
31};
32
33//
34// convert json dom values to text for display and editing
35// this is similar to db_sprintf()
36//
37function mie_to_string(tid, jvalue, format) {
38 if (tid === TID_BOOL) {
39
40 if (format && format[0] === "[") {
41 let col = "grey";
42
43 let cols = format.replace(/[\[\]]/g, "").split(",");
44 if (jvalue)
45 col = cols[1];
46 else
47 col = cols[0];
48
49 return "<span style=\"display: inline-block;" +
50 "width: 1em;height: 1em;" +
51 "border: 1px solid black;" +
52 "background-color: " + col + ";" +
53 "\"></span>";
54 }
55
56 if (jvalue)
57 return "y";
58 else
59 return "n";
60 }
61
62 if (Array.isArray(jvalue)) {
63 let s = "";
64 for (let jv of jvalue) {
65 s += mie_to_string(tid, jv, format);
66 s += ",";
67 }
68 s = s.slice(0, -1);
69 return s;
70 }
71
72 if (tid === TID_FLOAT || tid === TID_DOUBLE) {
73 if (jvalue === "NaN" || jvalue === "Infinity" || jvalue === "-Infinity")
74 return jvalue;
75
76 if (format && format.indexOf("%") !== -1) {
77 // new format
78 let i = format.indexOf('%');
79 let s1 = format.substring(0, format.indexOf('%'));
80 let s2, s3;
81 let p = parseInt(format[i+2]);
82 if (format[i+1] === "e")
83 s2 = Number(jvalue).toExponential(p);
84 if (format[i+1] === "p")
85 s2 = Number(jvalue).toPrecision(p);
86 if (format[i+1] === "f")
87 s2 = Number(jvalue).toFixed(p);
88 if (format[i+1] === "t")
89 s2 = jvalue.toLocaleString();
90 if (format[i+1] === "%")
91 s1 += '%';
92
93 i += 2;
94 while (format[i] >= '0' && format[i] <= '9')
95 i++;
96 s3 = format.substring(i);
97 return s1+s2+s3;
98
99 } else {
100
101 // old format
102 if (format && format.indexOf("e") !== -1) {
103 let p = parseInt(format.substring(format.indexOf("e") + 1));
104 return Number(jvalue).toExponential(p).toString();
105 }
106 if (format && format.indexOf("p") !== -1) {
107 let p = parseInt(format.substring(format.indexOf("p") + 1));
108 return Number(jvalue).toPrecision(p).toString();
109 }
110 if (format && format.indexOf("f") !== -1) {
111 let p = parseInt(format.substring(format.indexOf("f") + 1));
112 return Number(jvalue).toFixed(p).toString();
113 }
114 if (format && format.indexOf("t") !== -1) {
115 return jvalue.toLocaleString();
116 }
117 return jvalue.toString();
118 }
119 }
120
121 let t = typeof jvalue;
122
123 if (t === 'number') {
124 jvalue = "" + jvalue;
125 }
126
127 if (tid === TID_UINT64 || tid === TID_INT64 ||
128 tid === TID_DWORD || tid === TID_INT ||
129 tid === TID_WORD || tid === TID_SHORT || tid === TID_BYTE) {
130
131 if (jvalue.substring(0, 2) === '0x')
132 jvalue = "" + parseInt(jvalue, 16);
133
134 if (format && format.indexOf("%") !== -1) {
135 // new format
136 if (format.length === 1)
137 format = "%d";
138 let str = "";
139 for (let i = 0; i < format.length; i++) {
140 if (format[i] === '%') {
141 i++;
142
143 if (format[i] === 'd')
144 str += jvalue;
145 if (format[i] === 'e') {
146 let p = parseInt(format.substring(i+1));
147 str += Number(jvalue).toExponential(p).toString();
148 }
149 if (format[i] === 'p') {
150 let p = parseInt(format.substring(i+1));
151 str += Number(jvalue).toPrecision(p).toString();
152 }
153 if (format[i] === 'f') {
154 let p = parseInt(format.substring(i+1));
155 str += Number(jvalue).toFixed(p).toString();
156 }
157 if (format[i] === 'x')
158 str += "0x" + parseInt(jvalue).toString(16).toUpperCase();
159 if (format[i] === 'b')
160 str += parseInt(jvalue).toString(2) + "b";
161 if (format[i] === 't')
162 str += parseInt(jvalue).toLocaleString();
163 if (format[i] === '%')
164 str += '%';
165
166 i++;
167 while (format[i] >= '0' && format[i] <= '9')
168 i++;
169 i--;
170
171 } else
172 str += format[i];
173 }
174
175 return str;
176
177 } else {
178
179 //old format
180 if (format === undefined)
181 format = "d";
182 let str = "";
183 for (let i = 0; i < format.length; i++) {
184 if (format[i] === "d") {
185 if (str.length > 0)
186 str += " / " + jvalue;
187 else
188 str = jvalue;
189 }
190 if (format[i] === "x") {
191 let hex = parseInt(jvalue).toString(16).toUpperCase();
192 if (str.length > 0)
193 str += " / 0x" + hex;
194 else
195 str = "0x" + hex;
196 }
197 if (format[i] === "b") {
198 let bin = parseInt(jvalue).toString(2);
199 if (str.length > 0)
200 str += " / " + bin + "b";
201 else
202 str = bin + "b";
203 }
204 if (format[i] === "t") {
205 let loc = parseInt(jvalue).toLocaleString();
206 if (str.length > 0)
207 str += " / " + loc;
208 else
209 str = loc;
210 }
211 }
212
213 return str;
214 }
215 }
216
217 if (t === 'string') {
218 return jvalue;
219 }
220
221 return jvalue + " (" + t + ")";
222}
223
224//
225// stupid javascript does not have a function
226// to escape javascript and html characters
227// to make it safe to assign a json string
228// to p.innerHTML. What gives? K.O.
229//
230function mhttpd_escape(s) {
231 let ss = s;
232
233 if (typeof s !== 'string')
234 return ss;
235
236 while (ss.indexOf('"') >= 0)
237 ss = ss.replace('"', '&quot;');
238
239 while (ss.indexOf('>') >= 0)
240 ss = ss.replace('>', '&gt;');
241
242 while (ss.indexOf('<') >= 0)
243 ss = ss.replace('<', '&lt;');
244
245 //console.log("mhttpd_escape: [" + s + "] becomes [" + ss + "]");
246 return ss;
247}
248
249//
250// odb inline edit - make element a link to inline editor
251//
252function mie_back_to_link(p, path) {
253 let link = document.createElement('a');
254 link.href = path + "?cmd=Set";
255 link.innerHTML = "(loading...)";
256
257 mjsonrpc_db_get_values([path]).then(function (rpc) {
258 let value = rpc.result.data[0];
259 let tid = rpc.result.tid[0];
260 let mvalue = mie_to_string(tid, value, p.dataset.format);
261 if (mvalue === "")
262 mvalue = "(empty)";
263 link.innerHTML = mhttpd_escape(mvalue);
264 link.onclick = function () {
265 ODBInlineEdit(p, path);
266 return false;
267 };
268 link.onfocus = function () {
269 ODBInlineEdit(p, path);
270 };
271 link.style.color = p.oldStyle.color;
272
273 if (p.childNodes[1] !== undefined &&
274 p.childNodes[1].value !== undefined) //two values means it was editing an array
275 setTimeout(function () {
276 p.appendChild(link);
277 p.removeChild(p.childNodes[1]);
278 if (p.refreshPage)
279 p.refreshPage();
280 }, 10);
281 else
282 setTimeout(function () {
283 p.appendChild(link);
284 p.removeChild(p.childNodes[0]);
285 if (p.refreshPage)
286 p.refreshPage();
287 }, 10);
288
289
290 if (p.value !== value) {
291 p.value = value;
292
293 if (p.onchange !== null) {
294 p.onchange();
295 }
296 }
297
298 }).catch(function (error) {
299 mjsonrpc_error_alert(error);
300 });
301}
302
303//
304// called from ODBFinishInlineEdit if element has 'confirm' flag
305//
306function ODBConfirmInlineEdit(flag, p) {
307
308 let value;
309 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
310 value = p.childNodes[1].value;
311 else
312 value = p.childNodes[0].value;
313
314 if (flag === true) {
315 mjsonrpc_db_set_value(p.odbPath, value).then(function (rpc) {
316 if (parseInt(p.dataset.input) !== 1) {
317 p.ODBsent = true;
318 mie_back_to_link(p, p.odbPath);
319 }
320 }).catch(function (error) {
321 mjsonrpc_error_alert(error);
322 });
323 } else {
324 if (parseInt(p.dataset.input) !== 1) {
325 p.ODBsent = true;
326 mie_back_to_link(p, p.odbPath);
327 }
328 }
329}
330
331
332//
333// odb inline edit - write new value to odb
334//
335function ODBFinishInlineEdit(p, path) {
336 let value;
337
338 if (p.ODBsent === true)
339 return;
340
341 if (!p.inEdit)
342 return;
343
344 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
345 value = p.childNodes[1].value;
346 else
347 value = p.childNodes[0].value;
348
349 //console.log("mie_write odb [" + path + "] value [" + value + "]");
350
351 // check for validation
352 if (p.dataset.validate !== undefined) {
353 let flag = eval(p.dataset.validate)(value, p);
354 if (!flag) {
355 if (parseInt(p.dataset.input) !== 1) {
356 p.ODBsent = true;
357 p.inEdit = false;
358 mie_back_to_link(p, path);
359 }
360 return;
361 }
362 }
363
364 // validator might have changed the value
365 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
366 value = p.childNodes[1].value;
367 else
368 value = p.childNodes[0].value;
369
370 // check for confirm
371 if (p.dataset.confirm !== undefined) {
372 p.inEdit = false;
373 dlgConfirm(p.dataset.confirm, ODBConfirmInlineEdit, p);
374 return;
375 }
376
377 mjsonrpc_db_set_value(path, value).then(function (rpc) {
378 if (parseInt(p.dataset.input) !== 1) {
379 p.ODBsent = true;
380 p.inEdit = false;
381 mie_back_to_link(p, path);
382 }
383 }).catch(function (error) {
384 mjsonrpc_error_alert(error);
385 });
386}
387
388//
389// odb inline edit - key-press handler
390//
391function ODBInlineEditKeydown(event, p, path) {
392 let keyCode = ('which' in event) ? event.which : event.keyCode;
393
394 if (keyCode === 27) {
395 /* cancel editing */
396 if (parseInt(p.dataset.input) !== 1) {
397 p.ODBsent = true;
398 p.inEdit = false;
399 mie_back_to_link(p, path);
400 }
401 return false;
402 }
403
404 if (keyCode === 13) {
405 ODBFinishInlineEdit(p, path);
406 return false;
407 }
408
409 return true;
410}
411
412//
413// odb inline edit - convert link to edit field
414//
415function mie_link_to_edit(p, cur_val, size) {
416 let string_val = String(cur_val);
417
418 p.ODBsent = false;
419 if (p.tagName === 'A')
420 p.oldStyle = p.style;
421 else if (p.getElementsByTagName('a').length > 0)
422 p.oldStyle = p.getElementsByTagName('a')[0].style;
423
424 if (size === undefined)
425 size = 20;
426 let str = mhttpd_escape(string_val);
427
428 let index = "";
429 if (p.innerHTML[0] === '[' && p.innerHTML.indexOf(']') !== 0)
430 index = p.innerHTML.substring(0, p.innerHTML.indexOf(']')+1) + "&nbsp;";
431
432 p.innerHTML = index + "<input type='text' size='" + size + "' value='" + str +
433 "' onKeydown='return ODBInlineEditKeydown(event, this.parentNode,&quot;" +
434 p.odbPath + "&quot;," + ");' onBlur='ODBFinishInlineEdit(this.parentNode,&quot;" +
435 p.odbPath + "&quot;," + ");' >";
436
437 // needed for Firefox
438 if (parseInt(p.dataset.input) !== 1)
439 setTimeout(function () {
440 if (index === "") {
441 p.childNodes[0].focus();
442 p.childNodes[0].select();
443 } else {
444 p.childNodes[1].focus();
445 p.childNodes[1].select();
446 }
447 }, 10);
448}
449
450//
451// odb inline edit - start editing
452//
453function ODBInlineEdit(p, odb_path) {
454 if (p.inEdit)
455 return;
456 p.inEdit = true;
457 p.odbPath = odb_path;
458
459 mjsonrpc_db_get_values([odb_path]).then(function (rpc) {
460 let value = rpc.result.data[0];
461 if (Array.isArray(value))
462 value = value[0];
463 let tid = rpc.result.tid[0];
464 let format = p.dataset.format;
465 if (format === '')
466 format = undefined;
467 let size = p.dataset.size;
468 if (size === undefined) {
469 size = value.length;
470 if (size === undefined || size < 20)
471 size = 20;
472 }
473 if (format) {
474 if (format.length > 1) {
475 if (format[0] === 'd' || format[0] === 'x' || format[0] === 'b') {
476 //when going to edit consider only the first format specifier for integers
477 format = String(format[0]);
478 }
479 }
480 if (format == 't') {
481 // JSON parser can't cope with locale-based strings (thousands separators), so
482 // force the user to edit the value as a regular number.
483 format = undefined;
484 }
485 }
486 let mvalue = mie_to_string(tid, value, format);
487 mie_link_to_edit(p, mvalue, size);
488 }).catch(function (error) {
489 mjsonrpc_error_alert(error);
490 });
491}
492
493function dlgOdbEditKeydown(event, input) {
494 let keyCode = ('which' in event) ? event.which : event.keyCode;
495
496 if (keyCode === 27) {
497 // cancel editing
498 dlgMessageDestroy(input.parentElement.parentElement);
499 return false;
500 }
501
502 if (keyCode === 13) {
503 dlgOdbEditSend(input.parentElement);
504 dlgMessageDestroy(input.parentElement.parentElement);
505 return false;
506 }
507
508 return true;
509}
510
511function dlgOdbEditSend(b) {
512 let path = b.parentElement.parentElement.parentElement.odbPath;
513 let value = b.parentElement.parentElement.elements[0].value;
514
515 mjsonrpc_db_set_value(path, value).then(function (rpc) {
516 //mjsonrpc_debug_alert(rpc);
517 }).catch(function (error) {
518 mjsonrpc_error_alert(error);
519 });
520}
521
522//
523// odb edit single value dialog box
524//
525function dlgOdbEdit(path) {
526
527 mjsonrpc_db_get_value(path).then(function (rpc) {
528 let value = rpc.result.data[0];
529 let tid = rpc.result.tid[0];
530 value = mie_to_string(tid, value);
531
532 d = document.createElement("div");
533 d.className = "dlgFrame";
534 d.style.zIndex = 20;
535 d.odbPath = path;
536
537 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">Change ODB value</div>" +
538 "<form>" +
539 "<div class=\"dlgPanel\" style=\"padding: 30px;\">" +
540 "<div id=\"dlgMessageString\">" +
541 path + "&nbsp;:&nbsp;&nbsp;" +
542 "<input type='text' size='16' value='" + value + "' onkeydown='return dlgOdbEditKeydown(event, this);'>" +
543 "</div>" +
544 "<br /><br />" +
545 "<button class=\"dlgButton\" id=\"dlgMessageButton\" style=\"background-color:#F8F8F8\" type=\"button\" " +
546 " onClick=\"dlgOdbEditSend(this);dlgMessageDestroy(this.parentElement);\">Ok</button>" +
547 "<button class=\"dlgButton\" id=\"dlgMessageButton\" style=\"background-color:#F8F8F8\" type=\"button\" " +
548 " onClick=\"dlgMessageDestroy(this.parentElement)\">Cancel</button>" +
549 "</div>" +
550 "</form>";
551
552 document.body.appendChild(d);
553
554 dlgShow(d, false);
555
556 // needed for Firefox
557 setTimeout(function () {
558 d.childNodes[1].elements[0].focus();
559 d.childNodes[1].elements[0].select();
560 }, 10);
561
562
563 }).catch(function (error) {
564 mjsonrpc_error_alert(error);
565 });
566}
567
568/*---- mhttpd functions -------------------------------------*/
569
570//
571// Returns time to display in status bar and history.
572// By default show local time, but through the config page the
573// user can select the server time zone or any other time zone
574//
575function mhttpd_get_display_time(sec) {
576 // retrieve timezone of server on the first call
577 if (serverTimezoneOffset === undefined) {
578 mjsonrpc_get_timezone().then(function (rpc) {
579 serverTimezoneOffset = rpc.result;
580 }).catch(function (error) {
581 mjsonrpc_error_alert(error);
582 });
583 }
584
585 let d;
586 if (sec !== undefined)
587 d = new Date(sec * 1000);
588 else
589 d = new Date();
590 let o = 0;
591
592 if (mhttpdConfig().timezone === undefined)
593 mhttpdConfigSet('timezone', 'local');
594
595 if (mhttpdConfig().timezone === 'local') {
596 o = -d.getTimezoneOffset() / 60;
597 } else if (mhttpdConfig().timezone === 'server') {
598 if (serverTimezoneOffset === undefined)
599 return {
600 date: undefined,
601 string: "",
602 offset: 0
603 };
604 o = serverTimezoneOffset / 3600;
605 } else {
606 o = parseInt(mhttpdConfig().timezone);
607 }
608
609 d = new Date(d.getTime() + o * 3600 * 1000);
610
611 let s = d.toLocaleString("en-gb", {
612 timeZone: 'UTC',
613 hour12: false, day: 'numeric', month: 'short', year: 'numeric',
614 hour: 'numeric', minute: 'numeric', second: 'numeric'
615 });
616
617 if (o === 0)
618 s += " UTC";
619 else if (o > 0)
620 s += " UTC+"+o;
621 else
622 s += " UTC"+o;
623
624 return {
625 date: d,
626 string: s,
627 offset: o
628 };
629}
630
631function mhttpd_disable_button(button) {
632 if (!button.disabled) {
633 button.disabled = true;
634 }
635}
636
637function mhttpd_enable_button(button) {
638 if (button.disabled) {
639 button.disabled = false;
640 }
641}
642
643function mhttpd_set_style_visibility(e, v) {
644 if (e) {
645 if (e.style.visibility !== v) {
646 e.style.visibility = v;
647 }
648 }
649}
650
651function mhttpd_set_style_display(e, v) {
652 if (e) {
653 if (e.style.display !== v) {
654 e.style.display = v;
655 }
656 }
657}
658
659function mhttpd_set_firstChild_data(e, v) {
660 if (e) {
661 if (e.firstChild.data !== v) {
662 //console.log("mhttpd_set_firstChild_data for " + e + " from " + e.firstChild.data + " to " + v);
663 e.firstChild.data = v;
664 }
665 }
666}
667
668function mhttpd_set_innerHTML(e, v) {
669 if (e) {
670 if (e.innerHTML !== v) {
671 //console.log("mhttpd_set_innerHTML for " + e + " from " + e.innerHTML + " to " + v);
672 e.innerHTML = v;
673 }
674 }
675}
676
677function mhttpd_set_className(e, v) {
678 if (e) {
679 if (e.className !== v) {
680 //console.log("mhttpd_set_className for " + e + " from " + e.className + " to " + v);
681 e.className = v;
682 }
683 }
684}
685
686function mhttpd_hide_button(button) {
687 mhttpd_set_style_visibility(button, "hidden");
688 mhttpd_set_style_display(button, "none");
689}
690
691function mhttpd_unhide_button(button) {
692 mhttpd_set_style_visibility(button, "visible");
693 mhttpd_set_style_display(button, "");
694}
695
696function mhttpd_hide(id) {
697 let e = document.getElementById(id);
698 if (e) {
699 mhttpd_set_style_visibility(e, "hidden");
700 mhttpd_set_style_display(e, "none");
701 }
702}
703
704function mhttpd_unhide(id) {
705 let e = document.getElementById(id);
706 if (e) {
707 mhttpd_set_style_visibility(e, "visible");
708 mhttpd_set_style_display(e, "");
709 }
710}
711
712function mhttpd_enable(id) {
713 let e = document.getElementById(id);
714 if (e)
715 e.disabled = false;
716}
717
718function mhttpd_disable(id) {
719 let e = document.getElementById(id);
720 if (e)
721 e.disabled = true;
722}
723
724function mhttpd_init_overlay(overlay) {
725 mhttpd_hide_overlay(overlay);
726
727 // this element will hide the underlaying web page
728
729 overlay.style.zIndex = 10;
730 //overlay.style.backgroundColor = "rgba(0,0,0,0.5)"; /*dim the background*/
731 overlay.style.backgroundColor = "white";
732 overlay.style.position = "fixed";
733 overlay.style.top = "0%";
734 overlay.style.left = "0%";
735 overlay.style.width = "100%";
736 overlay.style.height = "100%";
737
738 return overlay.children[0];
739}
740
741function mhttpd_hide_overlay(overlay) {
742 overlay.style.visibility = "hidden";
743 overlay.style.display = "none";
744}
745
746function mhttpd_unhide_overlay(overlay) {
747 overlay.style.visibility = "visible";
748 overlay.style.display = "";
749}
750
751function mhttpd_getParameterByName(name) {
752 let match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
753 return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
754}
755
756function mhttpd_goto_page(page, param) {
757 if (!param) param = "";
758 window.location.href = '?cmd=' + page + param; // reloads the page from new URL
759 // DOES NOT RETURN
760}
761
762function mhttpd_navigation_bar(current_page, path) {
763 document.write("<div id='customHeader'>\n");
764 document.write("</div>\n");
765
766 document.write("<div class='mnav'>\n");
767 document.write(" <table>\n");
768 document.write(" <tr><td id='navigationTableButtons'></td></tr>\n");
769 document.write(" </table>\n\n");
770 document.write("</div>\n");
771
772 if (!path)
773 path = "";
774
775 if (localStorage.mNavigationButtons !== undefined) {
776 document.getElementById("navigationTableButtons").innerHTML = localStorage.mNavigationButtons;
777 let button = document.getElementById("navigationTableButtons").children;
778 for (let i = 0; i < button.length; i++)
779 if (button[i].value.toLowerCase() === current_page.toLowerCase())
780 button[i].className = "mnav mnavsel navButtonSel";
781 else
782 button[i].className = "mnav navButton";
783 return;
784 }
785
786 mjsonrpc_db_get_values(["/Custom/Header", "/Experiment/Menu", "/Experiment/Menu Buttons"]).then(function (rpc) {
787 let custom_header = rpc.result.data[0];
788
789 if (custom_header && custom_header.length > 0)
790 document.getElementById("customHeader").innerHTML = custom_header;
791
792 let menu = rpc.result.data[1];
793 let buttons = rpc.result.data[2];
794 let b = [];
795
796 if (menu) {
797 for (let k in menu) {
798 let kk = k + "/name";
799 if (kk in menu) {
800 if (menu[k]) {
801 b.push(menu[kk]);
802 }
803 }
804 }
805 } else if (buttons && buttons.length > 0) {
806 b = buttons.split(",");
807 }
808
809 if (!b || b.length < 1) {
810 b = ["Status", "ODB", "Messages", "Chat", "ELog", "Alarms", "Programs", "History", "MSCB", "Sequencer", "Config", "Help"];
811 }
812
813 let html = "";
814
815 for (let i = 0; i < b.length; i++) {
816 let bb = b[i].trim();
817 let cmd = bb;
818 let cc = "mnav navButton";
819 if (bb.toLowerCase() === current_page.toLowerCase()) {
820 cc = "mnav mnavsel navButtonSel";
821 }
822 if (bb == "PySequencer") {
823 cmd = "Sequencer&SeqODB=/PySequencer";
824 }
825 html += "<input type=button name=cmd value='" + bb + "' class='" + cc + "' onclick='window.location.href=\'" + path + "?cmd=" + cmd + "\';return false;'>\n";
826 }
827 document.getElementById("navigationTableButtons").innerHTML = html;
828
829 // cache navigation buttons in browser local storage
830 localStorage.setItem("mNavigationButtons", html);
831
832 }).catch(function (error) {
833 mjsonrpc_error_alert(error);
834 });
835}
836
837function mhttpd_show_menu(flag) {
838 let m = document.getElementById("msidenav");
839
840 if (m.initialWidth === undefined)
841 m.initialWidth = m.clientWidth;
842
843 if (flag) {
844 m.style.width = m.initialWidth + "px";
845 document.getElementById("mmain").style.marginLeft = m.initialWidth + "px";
846 mhttpdConfigSet('hideMenu', false);
847 } else {
848 m.style.width = "0";
849 document.getElementById("mmain").style.marginLeft = "0";
850 }
851
852 mhttpdConfigSet('showMenu', flag);
853}
854
855function mhttpd_toggle_menu() {
856 let flag = mhttpdConfig().showMenu;
857 flag = !flag;
858 mhttpd_show_menu(flag);
859}
860
861function mhttpd_exec_script(name) {
862 //console.log("exec_script: " + name);
863 let params = new Object;
864 params.script = name;
865 mjsonrpc_call("exec_script", params).then(function (rpc) {
866 let status = rpc.result.status;
867 if (status != 1) {
868 dlgAlert("Exec script \"" + name + "\" status " + status);
869 }
870 }).catch(function (error) {
871 mjsonrpc_error_alert(error);
872 });
873}
874
875let mhttpd_refresh_id;
876let mhttpd_refresh_history_id;
877let mhttpd_refresh_interval;
878let mhttpd_refresh_paused;
879let mhttpd_refresh_history_interval;
880let mhttpd_refresh_boost;
881let mhttpd_spinning_wheel;
882let mhttpd_initialized = false;
883
884function mhttpd_init(current_page, interval, callback) {
885 /*
886 This function should be called from custom pages to initialize all ODB tags and refresh
887 them periodically every "interval" in ms
888
889 For possible ODB tags please refer to
890
891 https://midas.triumf.ca/MidasWiki/index.php/New_Custom_Pages_(2017)
892 */
893
894 if (mhttpd_initialized) {
895 dlgAlert("mhttpd_init() is called more than once from user code");
896 return;
897 }
898
899 // initialize URL
900 url = mhttpd_getParameterByName("URL");
901 if (url)
902 mjsonrpc_set_url(url);
903
904 // retrieve current page if not given
905 if (current_page === undefined) {
906 current_page = mhttpd_getParameterByName("page");
907 }
908 if (current_page === undefined || current_page === null) {
909 dlgAlert("Please specify name of page via mhttpd_init('&lt;name&gt;')<br>or make sure to have '&amp;page=&lt;name&gt;' in URL");
910 return;
911 }
912
913 // create header
914 let h = document.getElementById("mheader");
915 if (h === null) {
916 dlgAlert('Web page does not contain "mheader" element');
917 return;
918 }
919 let s = document.getElementById("msidenav");
920 if (s === null) {
921 dlgAlert('Web page does not contain "msidenav" element');
922 return;
923 }
924
925 mhttpd_initialized = true;
926
927 // set global font size
928 let config = mhttpdConfig();
929 document.body.style.fontSize = config.fontSize + "pt";
930
931 h.style.display = "flex";
932 h.innerHTML =
933 "<div style='display:inline-block; flex:none;'>" +
934 "<span class='mmenuitem' style='padding: 5px;'><a href='https://midas.triumf.ca'>" +
935 "<img alt='MIDAS home page' src='favicon.png'></a></span>" +
936 "<span class='mmenuitem' style='padding: 5px;margin-right: 20px;' onclick='mhttpd_toggle_menu()'>&#9776;</span>" +
937 "<span id='mheader_expt_name'></span>" +
938 "</div>" +
939
940 "<div style='flex:auto;'>" +
941 " <div id='mheader_message'></div>" +
942 "</div>" +
943
944 "<div style='display: inline; flex:none;'>" +
945 " <div id='mheader_alarm'>&nbsp;</div>" +
946 " <div style='display: inline; font-size: 75%; margin-right: 10px' id='mheader_last_updated'>mheader_last_updated</div>" +
947 "</div>";
948
949 mhttpd_resize_sidenav();
950 window.addEventListener('resize', mhttpd_resize_sidenav);
951
952 // put error header in front of header
953 let d = document.createElement('div');
954 d.id = 'mheader_error';
955 h.parentNode.insertBefore(d, h);
956
957 // update header and menu
958 if (document.getElementById("msidenav") !== undefined) {
959
960 // get it from session storage cache if present
961 if (sessionStorage.msidenav !== undefined && sessionStorage.mexpname !== undefined) {
962 let menu = document.getElementById("msidenav");
963 menu.innerHTML = sessionStorage.msidenav;
964 let item = menu.children;
965 for (let i = 0; i < item.length; i++) {
966 if (item[i].className !== "mseparator") {
967 if (current_page.search(item[i].innerHTML) !== -1)
968 item[i].className = "mmenuitem mmenuitemsel";
969 else
970 item[i].className = "mmenuitem";
971 }
972 }
973 document.getElementById("mheader_expt_name").innerHTML = sessionStorage.mexpname;
974
975 // now the side navigation has its full width, adjust the main body and make it visible
976 let m = document.getElementById("mmain");
977 if (m !== undefined) {
978 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
979 m.style.opacity = "1";
980 }
981 }
982
983 // request it from server, since it might have changed
984 mjsonrpc_db_get_values(["/Experiment/Name", "/Experiment/Menu", "/Experiment/Menu Buttons",
985 "/Custom", "/Script", "/Alias"]).then(function (rpc) {
986
987 let expt_name = rpc.result.data[0];
988 let menu = rpc.result.data[1];
989 let buttons = rpc.result.data[2];
990 let custom = rpc.result.data[3];
991 let script = rpc.result.data[4];
992 let alias = rpc.result.data[5];
993
994 document.getElementById("mheader_expt_name").innerHTML = expt_name;
995 sessionStorage.setItem("mexpname", expt_name);
996
997 // preload spinning wheel for later use
998 if (mhttpd_spinning_wheel === undefined) {
999 mhttpd_spinning_wheel = new Image();
1000 mhttpd_spinning_wheel.src = "spinning-wheel.gif";
1001 }
1002
1003 // menu buttons
1004 let b = [];
1005 if (menu) {
1006 for (let k in menu) {
1007 if (k.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1008 continue;
1009 if (menu[k]) // show button if not disabled
1010 b.push(menu[k + "/name"]);
1011 }
1012 } else if (buttons && buttons.length > 0) {
1013 b = buttons.split(",");
1014 }
1015
1016 if (!b || b.length < 1) {
1017 b = ["Status", "ODB", "Messages", "Chat", "ELog", "Alarms", "Programs", "History", "MSCB", "Sequencer", "Config", "Help"];
1018 }
1019
1020 let html = "";
1021
1022 for (let i = 0; i < b.length; i++) {
1023 let bb = b[i].trim();
1024 let cc = "mmenuitem";
1025 let cmd = bb;
1026 if (bb === current_page) {
1027 cc += " mmenuitemsel";
1028 }
1029 if (bb == "PySequencer") {
1030 cmd = "Sequencer&SeqODB=/PySequencer";
1031 }
1032 html += "<div class='" + cc + "'><a href='?cmd=" + cmd + "' class='mmenulink'>" + bb + "</a></div>\n";
1033 }
1034
1035 // custom
1036 if (custom !== null && Object.keys(custom).length > 0) {
1037 // add separator
1038 html += "<div class='mseparator'></div>\n";
1039 // add menu items recursively
1040 html = mhttpd_add_menu_items(html, custom, current_page, "", 0);
1041 }
1042
1043 // script
1044 if (script !== null && Object.keys(script).length > 0) {
1045 // add separator
1046 html += "<div class='mseparator'></div>\n";
1047
1048 for (let b in script) {
1049 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1050 continue;
1051 let n = script[b + "/name"];
1052 //html += "<div class='mmenuitem'><a href='?script=" + b + "' class='mmenulink'>" + n + "</a></div>\n";
1053 html += "<div class='mmenuitem'><button class='mbutton' onclick='mhttpd_exec_script(\"" + n + "\")'>" + n + "</button></div>\n";
1054 }
1055
1056 }
1057
1058 // alias
1059 if (alias !== null && Object.keys(alias).length > 0) {
1060 // add separator
1061 html += "<div class='mseparator'></div>\n";
1062
1063 for (b in alias) {
1064 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1065 continue;
1066 n = alias[b + "/name"];
1067 if (n.substr(n.length - 1) === "&") {
1068 n = n.substr(0, n.length - 1);
1069 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink' target='_blank'>" + n + "&#8599;</a></div>\n";
1070 } else {
1071 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink'>" + n + "</a></div>\n";
1072 }
1073 }
1074
1075 }
1076
1077 // dummy spacer to fix scrolling to the bottom, must be at least header height
1078 html += "<div style='height: 64px;'></div>\n";
1079
1080 document.getElementById("msidenav").innerHTML = html;
1081
1082 // re-adjust size of mmain element if menu has changed
1083 let m = document.getElementById("mmain");
1084 if (m !== undefined) {
1085 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
1086 m.style.opacity = "1";
1087 }
1088
1089 // cache navigation buttons in browser local storage
1090 sessionStorage.setItem("msidenav", html);
1091
1092 // show/hide sidenav according to local storage settings
1093 mhttpd_show_menu(mhttpdConfig().showMenu);
1094
1095 // hide all invisible menu items after layout has been finished
1096 let mi = document.getElementsByClassName('mmenuitem');
1097 for (m of mi) {
1098 let p = m.parentElement;
1099 if (p.style.visibility === "hidden") {
1100 p.style.visibility = "visible";
1101 p.style.display = "none";
1102 }
1103 }
1104 mi = document.getElementsByClassName('msubmenuitem');
1105 for (m of mi) {
1106 let p = m.parentElement;
1107 if (p.style.visibility === "hidden") {
1108 p.style.visibility = "visible";
1109 p.style.display = "none";
1110 }
1111 }
1112
1113 }).then(function () {
1114 if (callback !== undefined)
1115 callback();
1116 }).catch(function (error) {
1117 mjsonrpc_error_alert(error);
1118 });
1119 }
1120
1121 // add event handler to tabs
1122 const tabs = document.querySelectorAll('.mtab');
1123 const panels = document.querySelectorAll('.mpanel');
1124
1125 tabs.forEach(tab => {
1126 tab.addEventListener('click', () => {
1127 const id = tab.dataset.target;
1128
1129 tabs.forEach(t => t.classList.toggle('active', t === tab));
1130 panels.forEach(p => p.classList.toggle('active', p.id === id));
1131 });
1132 });
1133
1134 // store refresh interval and do initial refresh
1135 if (interval === undefined)
1136 interval = 1000;
1137 mhttpd_refresh_interval = interval;
1138 mhttpd_refresh_paused = false;
1139
1140 // history interval is static for now
1141 mhttpd_refresh_history_interval = 30000;
1142
1143 // trigger first refresh
1144 mhttpd_refresh();
1145 mhttpd_refresh_history();
1146}
1147
1148function mhttpd_find_menuitem(current_page, submenu) {
1149 for (let b in submenu) {
1150
1151 if (typeof submenu[b] === "object") { // recurse into submenu
1152 let flag = mhttpd_find_menuitem(current_page, submenu[b]);
1153 if (flag)
1154 return true;
1155 }
1156
1157 if (typeof submenu[b] === "string") {
1158 let s = submenu[b];
1159 if (s[s.length-1] === '&')
1160 s = s.slice(0, -1);
1161
1162 if (b.indexOf('/name') !== -1 && s.toLowerCase() === current_page.toLowerCase())
1163 return true;
1164
1165 }
1166 }
1167
1168 return false;
1169}
1170
1171function mhttpd_add_menu_items(html, custom, current_page, path, level) {
1172 for (let b in custom) {
1173 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1174 continue;
1175 if (b === "path" || b === "images" || b === "default") // skip "path", "Images" and "default"
1176 continue;
1177
1178 if (typeof custom[b] === "object") { // submenu created by subdirectory in ODB
1179 let cc = "msubmenuitem";
1180 let l = "";
1181 let expand = mhttpd_find_menuitem(current_page, custom[b]);
1182
1183 // add blanks according to nesting level
1184 for (let i=0 ; i<level ; i++)
1185 l += "&nbsp;&nbsp;";
1186
1187 if (expand)
1188 l += "&#9662;&nbsp;"; // caret down
1189 else
1190 l += "&#9656;&nbsp;"; // caret right
1191
1192 l += "&nbsp;";
1193 l += custom[b + "/name"];
1194 let p = path;
1195 if (p !== "")
1196 p += "/";
1197 p += b;
1198
1199 html += "<div class='" + cc + "' onclick='mhttpd_submenu(this)'><div class='mmenulink'>" + l + "</div></div>\n";
1200
1201 if (expand)
1202 html += "<div>"; // do not hide submenu if current page is under it
1203 else
1204 html += "<div style='visibility: hidden'>";
1205 html = mhttpd_add_menu_items(html, custom[b], current_page, p, level+1);
1206 html += "</div>";
1207 } else if (typeof custom[b] === "string") { // skip any items that don't have type of string, since can't be valid links
1208 let cc = "mmenuitem";
1209 let mitem = custom[b + "/name"];
1210 if (mitem.slice(-1) === '&')
1211 mitem = mitem.slice(0, -1);
1212 if (current_page.search(mitem) !== -1 ||
1213 current_page.search(mitem.toLowerCase()) !== -1)
1214 cc += " mmenuitemsel";
1215 let ln = "";
1216 for (let i = 0; i < level; i++)
1217 ln += "&nbsp;&nbsp;";
1218 ln += custom[b + "/name"];
1219
1220 if (custom[b].substring(0, 5) === "link:") {
1221 let lt = custom[b].substring(5);
1222 html += "<div class='" + cc + "'><a href='" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1223 } else {
1224 let lt = path;
1225 if (lt !== "")
1226 lt += "/";
1227 lt += custom[b + "/name"];
1228 if (ln.slice(-1) === '!')
1229 continue;
1230 let target = "";
1231 let style = "";
1232 let clicklink = false;
1233 // if (level > 0)
1234 // style = "style='display: none'";
1235
1236 // Accepted file extentions, if not create onclick call from value
1237 const validExtensionsRegex = /\.([a-zA-Z0-9]+)$/; // /\.(txt|jpg|jpeg|png|gif|pdf|csv|html|htm|js|css)$/i;
1238
1239 if (ln.slice(-1) === '&') {
1240 ln = ln.slice(0, -1);
1241 lt = lt.slice(0, -1);
1242 target += "target='_blank' ";
1243 } else if (!validExtensionsRegex.test(custom[b])) {
1244 clicklink = true;
1245 }
1246
1247 if (clicklink) {
1248 const handler = custom[b]
1249 .replace(/&/g, "&amp;") // must do ampersand first
1250 .replace(/"/g, "&quot;")
1251 .replace(/'/g, "&#39;");
1252 html += `<div ${style} class="mmenuitem" onclick="${handler}"><a style="width: 100%;display: inline-block;text-decoration: none;color: #404040;" class="mmenulink">${ln}</a></div>\n`;
1253 } else {
1254 html += "<div class='" + cc + "' " + style + "><a " + target + " href='?cmd=custom&page=" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1255 }
1256
1257 }
1258 }
1259 }
1260
1261 return html;
1262}
1263
1264function mhttpd_submenu(o) {
1265 let i;
1266 for (i=0 ; o.firstChild.innerHTML.charCodeAt(i) !== 9656 && o.firstChild.innerHTML.charCodeAt(i) !== 9662 ; i++);
1267 if (o.firstChild.innerHTML.charCodeAt(i) === 9656) {
1268 // expand
1269 o.firstChild.innerHTML =
1270 o.firstChild.innerHTML.substring(0, i-1) +
1271 String.fromCharCode(9662) +
1272 o.firstChild.innerHTML.substring(i+1);
1273 o.nextElementSibling.style.display = "inline";
1274 o.nextElementSibling.style.visibility = "visible";
1275 } else {
1276 // collapse
1277 o.firstChild.innerHTML =
1278 o.firstChild.innerHTML.substring(0, i-1) +
1279 String.fromCharCode(9656) +
1280 o.firstChild.innerHTML.substring(i+1);
1281 o.nextElementSibling.style.display = "none";
1282 }
1283}
1284
1285function mhttpd_refresh_pause(flag) {
1286 mhttpd_refresh_paused = flag;
1287}
1288
1289function mhttpd_set_refresh_interval(interval) {
1290 mhttpd_refresh_interval = interval;
1291}
1292
1293function getMElements(name) {
1294 // collect all <div name=[name] >
1295 let e = [];
1296 e.push(...document.getElementsByName(name));
1297
1298 // collect all <div class=[name] >
1299 e.push(...document.getElementsByClassName(name));
1300
1301 return e;
1302}
1303
1304function mhttpd_thermo_draw() {
1305
1306 if (this.value === undefined)
1307 return;
1308
1309 let ctx = this.firstChild.getContext("2d");
1310 ctx.save();
1311
1312 let w = this.firstChild.width;
1313 let h = this.firstChild.height;
1314
1315 ctx.clearRect(0, 0, w, h);
1316 w = w - 1; // space for full circles
1317 h = h - 1;
1318
1319 if (this.dataset.scale === "1") {
1320 w = w / 2;
1321 w = Math.floor(w / 4) * 4;
1322 }
1323
1324 if (this.dataset.printValue === "1") {
1325 h = h - 14;
1326 }
1327
1328 let x0 = Math.round(w / 4 * 0);
1329 let x1 = Math.round(w / 4 * 1);
1330 let x2 = Math.round(w / 4 * 2);
1331 let x3 = Math.round(w / 4 * 3);
1332
1333 let v = this.value;
1334 if (v < this.dataset.minValue)
1335 v = this.dataset.minValue;
1336 if (v > this.dataset.maxValue)
1337 v = this.dataset.maxValue;
1338 let yt = (h - 4 * x1) - (h - 5 * x1) * (v - this.dataset.minValue) / (this.dataset.maxValue - this.dataset.minValue);
1339
1340 ctx.translate(0.5, 0.5);
1341 ctx.strokeStyle = "#000000";
1342 ctx.fillStyle = "#FFFFFF";
1343 ctx.lineWidth = 1;
1344
1345 // outer "glass"
1346 ctx.beginPath();
1347 ctx.arc(x2, x1, x1, Math.PI, 0);
1348 ctx.lineTo(x3, h - x1 * 4);
1349 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1350 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1351 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1352 ctx.lineTo(x1, x1);
1353 ctx.stroke();
1354 if (this.dataset.backgroundColor !== undefined) {
1355 ctx.fillStyle = this.dataset.backgroundColor;
1356 ctx.fill();
1357 }
1358
1359 // inner "fluid"
1360 if (this.dataset.color === undefined) {
1361 ctx.strokeStyle = "#000000";
1362 ctx.fillStyle = "#000000";
1363 } else {
1364 ctx.strokeStyle = this.dataset.color;
1365 ctx.fillStyle = this.dataset.color;
1366 }
1367
1368 ctx.beginPath();
1369 ctx.moveTo(x1 + 3, yt);
1370 ctx.lineTo(x3 - 3, yt);
1371 ctx.lineTo(x3 - 3, h - x2);
1372 ctx.lineTo(x1 + 3, h - x2);
1373 ctx.lineTo(x1 + 3, yt);
1374 ctx.stroke();
1375 ctx.fill();
1376
1377 ctx.beginPath();
1378 ctx.arc(x2, h - x2, x2 - 4, 0, 2 * Math.PI);
1379 ctx.stroke();
1380 ctx.fill();
1381
1382 // re-draw outer "glass"
1383 ctx.strokeStyle = "#000000";
1384 ctx.fillStyle = "#FFFFFF";
1385 ctx.lineWidth = 1;
1386 ctx.beginPath();
1387 ctx.arc(x2, x1, x1, Math.PI, 0);
1388 ctx.lineTo(x3, h - x1 * 4);
1389 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1390 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1391 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1392 ctx.lineTo(x1, x1);
1393 ctx.stroke();
1394
1395 // optional scale
1396 if (this.dataset.scale === "1") {
1397 ctx.beginPath();
1398 ctx.moveTo(x3 + x1 / 2, x1);
1399 ctx.lineTo(x3 + x1, x1);
1400 ctx.moveTo(x3 + x1 / 2, h - 4 * x1);
1401 ctx.lineTo(x3 + x1, h - 4 * x1);
1402 ctx.stroke();
1403
1404 ctx.font = "12px sans-serif";
1405 ctx.fillStyle = "#000000";
1406 ctx.strokeStyle = "#000000";
1407 ctx.textBaseline = "middle";
1408
1409 let mintext = this.dataset.minValue;
1410 let maxtext = this.dataset.maxValue;
1411
1412 if (this.dataset.format !== undefined) {
1413 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1414 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1415 }
1416
1417 ctx.fillText(mintext, 4.5 * x1, h - 4 * x1, 3.5 * x1);
1418 ctx.fillText(maxtext, 4.5 * x1, x1, 3.5 * x1);
1419 }
1420
1421 // optional value display
1422 if (this.dataset.printValue === "1") {
1423 ctx.font = "12px sans-serif";
1424 ctx.fillStyle = "#000000";
1425 ctx.strokeStyle = "#000000";
1426 ctx.textBaseline = "bottom";
1427 ctx.textAlign = "center";
1428
1429 let valuetext = this.value;
1430
1431 if (this.dataset.format !== undefined) {
1432 valuetext = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1433 }
1434
1435 ctx.fillText(valuetext, x2, this.firstChild.height, 4 * x1);
1436 }
1437
1438 ctx.restore();
1439}
1440
1441function mhttpd_gauge_draw() {
1442
1443 if (this.value === undefined)
1444 return;
1445
1446 let ctx = this.firstChild.getContext("2d");
1447 ctx.save();
1448 let w = this.firstChild.width;
1449 let h = this.firstChild.height;
1450 let y = h;
1451 if (this.dataset.scale === "1")
1452 y -= 15;
1453 else
1454 y -= 1;
1455 ctx.clearRect(0, 0, w, h);
1456
1457 let v = this.value;
1458 if (v < Number(this.dataset.minValue))
1459 v = Number(this.dataset.minValue);
1460 if (v > Number(this.dataset.maxValue))
1461 v = Number(this.dataset.maxValue);
1462 v = (v - Number(this.dataset.minValue)) /
1463 (Number(this.dataset.maxValue) - Number(this.dataset.minValue));
1464
1465 ctx.translate(0.5, 0.5);
1466 ctx.strokeStyle = "#000000";
1467 ctx.fillStyle = "#FFFFFF";
1468 ctx.lineWidth = 1;
1469
1470 let gaugeColor = parseColorSpec(this.dataset.color); // can be simple color or array of pairs
1471
1472 // outer ring
1473 if (typeof gaugeColor === "string") {
1474 // single color
1475 ctx.beginPath();
1476 ctx.arc(w / 2, y, w / 2 - 1, Math.PI, 0);
1477 ctx.lineTo(w - 4, y);
1478 ctx.arc(w / 2, y, w / 2 - 4, 0, Math.PI, true);
1479 ctx.lineTo(4, y);
1480 ctx.fillStyle = gaugeColor;
1481 ctx.fill();
1482
1483 } else {
1484
1485 // color segments
1486 for (let i=0 ; i<gaugeColor.length ; i++) {
1487 let startValue = gaugeColor[i].value;
1488 let startAngle = Math.PI + Math.PI * (startValue - Number(this.dataset.minValue)) /
1489 (Number(this.dataset.maxValue) - Number(this.dataset.minValue));
1490
1491 let endValue = i+1 < gaugeColor.length ? gaugeColor[i+1].value : Number(this.dataset.maxValue);
1492 let endAngle = Math.PI + Math.PI * (endValue - Number(this.dataset.minValue)) /
1493 (Number(this.dataset.maxValue) - Number(this.dataset.minValue));
1494
1495 let rOuter = w / 2 - 1;
1496 let rInner = rOuter - 3;
1497
1498 // leave little gaps between colors
1499 if (startAngle > Math.PI)
1500 startAngle += 0.02;
1501 if (endAngle < Math.PI * 2)
1502 endAngle -= 0.02;
1503
1504 ctx.beginPath();
1505 ctx.arc(w / 2, y, rOuter, startAngle, endAngle);
1506 ctx.lineTo(w / 2 + rInner * Math.cos(endAngle), y + rInner * Math.sin(endAngle));
1507 ctx.arc(w / 2, y, rInner, endAngle, startAngle, true);
1508 ctx.lineTo(w / 2 + rOuter * Math.cos(startAngle), y + rOuter * Math.sin(startAngle));
1509 ctx.fillStyle = gaugeColor[i].color;
1510 ctx.fill();
1511 }
1512 }
1513
1514 // background bar
1515 ctx.beginPath();
1516 ctx.fillStyle = this.dataset.backgroundColor ? this.dataset.backgroundColor : "#E0E0E0";
1517 ctx.arc(w / 2, y, w / 2 - 8, Math.PI, 2*Math.PI);
1518 ctx.arc(w / 2, y, w / 2 - w / 5, 2*Math.PI, Math.PI, true);
1519 ctx.fill();
1520
1521 // value bar
1522 if (typeof gaugeColor === "string") {
1523
1524 ctx.beginPath();
1525 ctx.fillStyle = gaugeColor;
1526 ctx.arc(w / 2, y, w / 2 - 8, Math.PI, (1 + v) * Math.PI);
1527 ctx.arc(w / 2, y, w / 2 - w / 5, (1 + v) * Math.PI, Math.PI, true);
1528 ctx.fill();
1529
1530 } else {
1531
1532 let i;
1533 for (i=0 ; i<gaugeColor.length-1 ; i++) {
1534 if (this.value >= gaugeColor[i].value && this.value < gaugeColor[i+1].value)
1535 break;
1536 }
1537
1538 ctx.beginPath();
1539 ctx.fillStyle = gaugeColor[i].color;
1540 ctx.arc(w / 2, y, w / 2 - 8, Math.PI, (1 + v) * Math.PI);
1541 ctx.arc(w / 2, y, w / 2 - w / 5, (1 + v) * Math.PI, Math.PI, true);
1542 ctx.fill();
1543 }
1544
1545 // needle
1546 ctx.beginPath();
1547 ctx.strokeStyle = "#404040";
1548 ctx.lineWidth = 2;
1549 ctx.moveTo(w / 2 + (w/2-1) * Math.cos((1 + v) * Math.PI), y + (w/2-1) * Math.sin((1 + v) * Math.PI));
1550 ctx.lineTo(w / 2 + (w/2-w/5)* Math.cos((1 + v) * Math.PI), y + (w/2-w/5) * Math.sin((1 + v) * Math.PI));
1551 ctx.stroke();
1552
1553 // black frame
1554 if (this.dataset.frame) {
1555 ctx.strokeStyle = "#000000";
1556 ctx.lineWidth = 1;
1557 ctx.beginPath();
1558 ctx.arc(w / 2, y, w / 2 - 6, Math.PI, 0);
1559 ctx.lineTo(w - w / 5, y);
1560 ctx.arc(w / 2, y, w / 2 - w / 5, 0, Math.PI, true);
1561 ctx.lineTo(6, y);
1562 ctx.stroke();
1563 }
1564
1565 // optional value display
1566 if (this.dataset.printValue === "1") {
1567 ctx.font = "12px sans-serif";
1568 ctx.fillStyle = "#000000";
1569 ctx.strokeStyle = "#000000";
1570 ctx.textBaseline = "bottom";
1571 ctx.textAlign = "center";
1572
1573 let valText = this.value;
1574
1575 if (this.dataset.format !== undefined)
1576 valText = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1577
1578 ctx.fillText(valText, w / 2, y, 0.5 * w);
1579 }
1580
1581 // optional scale display
1582 if (this.dataset.scale === "1") {
1583 ctx.font = "12px sans-serif";
1584 ctx.fillStyle = "#000000";
1585 ctx.strokeStyle = "#000000";
1586 ctx.textBaseline = "bottom";
1587 ctx.textAlign = "center";
1588
1589 let mintext = this.dataset.minValue;
1590 let maxtext = this.dataset.maxValue;
1591
1592 if (this.dataset.format !== undefined) {
1593 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1594 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1595 }
1596
1597 ctx.fillText(mintext, 0.1 * w, h, 0.2 * w);
1598 ctx.fillText(maxtext, 0.9 * w, h, 0.2 * w);
1599 }
1600
1601 ctx.restore();
1602}
1603
1604function mhttpd_vaxis_draw() {
1605 let ctx = this.firstChild.getContext("2d");
1606 ctx.save();
1607 let w = this.firstChild.width;
1608 let h = this.firstChild.height;
1609 ctx.clearRect(0, 0, w, h);
1610
1611 let line = true;
1612 if (this.dataset.line === "0")
1613 line = false;
1614 let log = false;
1615 if (this.dataset.log === "1")
1616 log = true;
1617
1618 let scaleMin = 0;
1619 let scaleMax = 1;
1620 if (this.dataset.minValue !== undefined)
1621 scaleMin = parseFloat(this.dataset.minValue);
1622 if (log && scaleMin === 0)
1623 scaleMin = 1E-3;
1624 if (this.dataset.maxValue !== undefined)
1625 scaleMax = parseFloat(this.dataset.maxValue);
1626 if (scaleMin === scaleMax)
1627 scaleMax += 1;
1628
1629 ctx.translate(0.5, 0.5);
1630 ctx.strokeStyle = "#000000";
1631 ctx.fillStyle = "#FFFFFF";
1632 ctx.lineWidth = 1;
1633
1634 if (this.style.textAlign === "left") {
1635 ctx.translate(-0.5, -0.5);
1636 vaxisDraw(ctx, 0, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1637 } else {
1638 ctx.translate(-0.5, -0.5);
1639 vaxisDraw(ctx, w, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1640 }
1641
1642 ctx.restore();
1643}
1644
1645function mhttpd_haxis_draw() {
1646 let ctx = this.firstChild.getContext("2d");
1647 ctx.save();
1648 let w = this.firstChild.width;
1649 let h = this.firstChild.height;
1650 ctx.clearRect(0, 0, w, h);
1651
1652 let line = true;
1653 if (this.dataset.line === "0")
1654 line = false;
1655 let log = false;
1656 if (this.dataset.log === "1")
1657 log = true;
1658
1659 let scaleMin = 0;
1660 let scaleMax = 1;
1661 if (this.dataset.minValue !== undefined)
1662 scaleMin = parseFloat(this.dataset.minValue);
1663 if (log && scaleMin === 0)
1664 scaleMin = 1E-3;
1665 if (this.dataset.maxValue !== undefined)
1666 scaleMax = parseFloat(this.dataset.maxValue);
1667 if (scaleMin === scaleMax)
1668 scaleMax += 1;
1669
1670 ctx.translate(-0.5, 0);
1671 ctx.strokeStyle = "#000000";
1672 ctx.fillStyle = "#FFFFFF";
1673 ctx.lineWidth = 1;
1674
1675 if (this.style.verticalAlign === "top") {
1676 ctx.translate(0.5, 0.5);
1677 haxisDraw(ctx, 1, 0, w - 2, line, 4, 8, 10, 10, 0, scaleMin, scaleMax, log);
1678 } else {
1679 ctx.translate(0.5, -0.5);
1680 haxisDraw(ctx, 1, h, w - 2, line, 4, 8, 10, 20, 0, scaleMin, scaleMax, log);
1681 }
1682 ctx.restore();
1683}
1684
1685String.prototype.stripZeros = function () {
1686 let s = this.trim();
1687 if (s.search("[.]") >= 0) {
1688 let i = s.search("[e]");
1689 if (i >= 0) {
1690 while (s.charAt(i - 1) === "0") {
1691 s = s.substring(0, i - 1) + s.substring(i);
1692 i--;
1693 }
1694 if (s.charAt(i - 1) === ".")
1695 s = s.substring(0, i - 1) + s.substring(i);
1696 } else {
1697 while (s.charAt(s.length - 1) === "0")
1698 s = s.substring(0, s.length - 1);
1699 if (s.charAt(s.length - 1) === ".")
1700 s = s.substring(0, s.length - 1);
1701 }
1702 }
1703 return s;
1704};
1705
1706function haxisDraw(ctx, x1, y1, width, line, minor, major, text, label, grid, xmin, xmax, logaxis) {
1707 let dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
1708 let tick_base, major_base, label_base, n_sig1, n_sig2, xs;
1709 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1710
1711 ctx.textAlign = "center";
1712 ctx.textBaseline = "top";
1713
1714 if (xmax <= xmin || width <= 0)
1715 return;
1716
1717 if (logaxis) {
1718 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1719 if (dx === 0)
1720 dx = 1E-10;
1721 label_dx = dx;
1722 major_dx = dx * 10;
1723 n_sig1 = 4;
1724 } else {
1725 /* use 6 as min tick distance */
1726 dx = (xmax - xmin) / (width / 6);
1727
1728 int_dx = Math.floor(Math.log(dx) / Math.log(10));
1729 frac_dx = Math.log(dx) / Math.log(10) - int_dx;
1730
1731 if (frac_dx < 0) {
1732 frac_dx += 1;
1733 int_dx -= 1;
1734 }
1735
1736 tick_base = frac_dx < (Math.log(2) / Math.log(10)) ? 1 : frac_dx < (Math.log(5) / Math.log(10)) ? 2 : 3;
1737 major_base = label_base = tick_base + 1;
1738
1739 /* rounding up of dx, label_dx */
1740 dx = Math.pow(10, int_dx) * base[tick_base];
1741 major_dx = Math.pow(10, int_dx) * base[major_base];
1742 label_dx = major_dx;
1743
1744 do {
1745 /* number of significant digits */
1746 if (xmin === 0)
1747 n_sig1 = 0;
1748 else
1749 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1750
1751 if (xmax === 0)
1752 n_sig2 = 0;
1753 else
1754 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1755
1756 n_sig1 = Math.max(n_sig1, n_sig2);
1757
1758 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1759 if (Math.abs(xmin) < 100000)
1760 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) + 1);
1761 if (Math.abs(xmax) < 100000)
1762 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) + 1);
1763
1764 /* determination of maximal width of labels */
1765 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
1766 let ext = ctx.measureText(str);
1767 maxwidth = ext.width;
1768
1769 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
1770 ext = ctx.measureText(str);
1771 maxwidth = Math.max(maxwidth, ext.width);
1772 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
1773 maxwidth = Math.max(maxwidth, ext.width);
1774
1775 /* increasing label_dx, if labels would overlap */
1776 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
1777 label_base++;
1778 label_dx = Math.pow(10, int_dx) * base[label_base];
1779 if (label_base % 3 === 2 && major_base % 3 === 1) {
1780 major_base++;
1781 major_dx = Math.pow(10, int_dx) * base[major_base];
1782 }
1783 } else
1784 break;
1785
1786 } while (true);
1787 }
1788
1789 if (y1 > 0) {
1790 minor = -minor;
1791 major = -major;
1792 text = -text;
1793 label = -label;
1794 }
1795
1796 x_act = Math.floor(xmin / dx) * dx;
1797
1798 let last_label_x = x1;
1799
1800 if (line === true)
1801 ctx.drawLine(x1, y1, x1 + width, y1);
1802
1803 do {
1804 if (logaxis)
1805 x_screen = (Math.log(x_act) - Math.log(xmin)) / (Math.log(xmax) - Math.log(xmin)) * width + x1;
1806 else
1807 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
1808 xs = Math.floor(x_screen + 0.5);
1809
1810 if (x_screen > x1 + width + 0.001)
1811 break;
1812
1813 if (x_screen >= x1) {
1814 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
1815 dx / major_dx / 10.0) {
1816
1817 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
1818 dx / label_dx / 10.0) {
1819 /* label tick mark */
1820 ctx.drawLine(xs, y1, xs, y1 + text);
1821
1822 /* grid line */
1823 if (grid != 0 && xs > x1 && xs < x1 + width)
1824 ctx.drawLine(xs, y1, xs, y1 + grid);
1825
1826 /* label */
1827 if (label != 0) {
1828 str = x_act.toPrecision(n_sig1).stripZeros();
1829 ext = ctx.measureText(str);
1830 ctx.save();
1831 ctx.fillStyle = "black";
1832 if (xs - ext.width / 2 > x1 &&
1833 xs + ext.width / 2 < x1 + width)
1834 ctx.fillText(str, xs, y1 + label);
1835 ctx.restore();
1836 last_label_x = xs + ext.width / 2;
1837 }
1838 } else {
1839 /* major tick mark */
1840 ctx.drawLine(xs, y1, xs, y1 + major);
1841
1842 /* grid line */
1843 if (grid != 0 && xs > x1 && xs < x1 + width)
1844 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
1845 }
1846
1847 if (logaxis) {
1848 dx *= 10;
1849 major_dx *= 10;
1850 label_dx *= 10;
1851 }
1852 } else
1853 /* minor tick mark */
1854 ctx.drawLine(xs, y1, xs, y1 + minor);
1855
1856 /* for logaxis, also put labes on minor tick marks */
1857 if (logaxis) {
1858 if (label != 0) {
1859 str = x_act.toPrecision(n_sig1).stripZeros();
1860 ext = ctx.measureText(str);
1861 ctx.save();
1862 ctx.fillStyle = "black";
1863 if (xs - ext.width / 2 > x1 &&
1864 xs + ext.width / 2 < x1 + width &&
1865 xs - ext.width / 2 > last_label_x + 2)
1866 ctx.fillText(str, xs, y1 + label);
1867 ctx.restore();
1868
1869 last_label_x = xs + ext.width / 2;
1870 }
1871 }
1872 }
1873
1874 x_act += dx;
1875
1876 /* supress 1.23E-17 ... */
1877 if (Math.abs(x_act) < dx / 100)
1878 x_act = 0;
1879
1880 } while (1);
1881}
1882
1883
1884function vaxisDraw(ctx, x1, y1, height, line, minor, major, text, label, grid, ymin, ymax, logaxis) {
1885 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen, maxwidth;
1886 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
1887 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1888
1889 if (x1 > 0)
1890 ctx.textAlign = "right";
1891 else
1892 ctx.textAlign = "left";
1893 ctx.textBaseline = "middle";
1894 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
1895
1896 if (ymax <= ymin || height <= 0)
1897 return;
1898
1899 if (logaxis) {
1900 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
1901 if (dy === 0)
1902 dy = 1E-10;
1903 label_dy = dy;
1904 major_dy = dy * 10;
1905 n_sig1 = 4;
1906 } else {
1907 /* use 6 as min tick distance */
1908 dy = (ymax - ymin) / (height / 6);
1909
1910 int_dy = Math.floor(Math.log(dy) / Math.log(10));
1911 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
1912
1913 if (frac_dy < 0) {
1914 frac_dy += 1;
1915 int_dy -= 1;
1916 }
1917
1918 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
1919 major_base = label_base = tick_base + 1;
1920
1921 /* rounding up of dy, label_dy */
1922 dy = Math.pow(10, int_dy) * base[tick_base];
1923 major_dy = Math.pow(10, int_dy) * base[major_base];
1924 label_dy = major_dy;
1925
1926 /* number of significant digits */
1927 if (ymin === 0)
1928 n_sig1 = 0;
1929 else
1930 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1931
1932 if (ymax === 0)
1933 n_sig2 = 0;
1934 else
1935 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1936
1937 n_sig1 = Math.max(n_sig1, n_sig2);
1938
1939 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1940 if (Math.abs(ymin) < 100000)
1941 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) + 1);
1942 if (Math.abs(ymax) < 100000)
1943 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) + 1);
1944
1945 /* increase label_dy if labels would overlap */
1946 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
1947 label_base++;
1948 label_dy = Math.pow(10, int_dy) * base[label_base];
1949 if (label_base % 3 === 2 && major_base % 3 === 1) {
1950 major_base++;
1951 major_dy = Math.pow(10, int_dy) * base[major_base];
1952 }
1953 }
1954 }
1955
1956 if (x1 > 0) {
1957 minor = -minor;
1958 major = -major;
1959 text = -text;
1960 label = -label;
1961 }
1962
1963 y_act = Math.floor(ymin / dy) * dy;
1964
1965 let last_label_y = y1;
1966
1967 if (line === true)
1968 ctx.drawLine(x1, y1, x1, y1 - height);
1969
1970 do {
1971 if (logaxis)
1972 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) / (Math.log(ymax) - Math.log(ymin)) * height;
1973 else
1974 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
1975 ys = Math.floor(y_screen + 0.5);
1976
1977 if (y_screen < y1 - height - 0.001)
1978 break;
1979
1980 if (y_screen <= y1 + 0.001) {
1981 if (Math.abs(Math.floor(y_act / major_dy + 0.5) - y_act / major_dy) <
1982 dy / major_dy / 10.0) {
1983
1984 if (Math.abs(Math.floor(y_act / label_dy + 0.5) - y_act / label_dy) <
1985 dy / label_dy / 10.0) {
1986 /* label tick mark */
1987 ctx.drawLine(x1, ys, x1 + text, ys);
1988
1989 /* grid line */
1990 if (grid != 0 && ys < y1 && ys > y1 - height)
1991 ctx.drawLine(x1, ys, x1 + grid, ys);
1992
1993 /* label */
1994 if (label != 0) {
1995 str = y_act.toPrecision(n_sig1).stripZeros();
1996 ctx.save();
1997 ctx.fillStyle = "black";
1998 if (ys - textHeight / 2 > y1 - height &&
1999 ys + textHeight / 2 < y1)
2000 ctx.fillText(str, x1 + label, ys);
2001 ctx.restore();
2002 last_label_y = ys - textHeight / 2;
2003 }
2004 } else {
2005 /* major tick mark */
2006 ctx.drawLine(x1, ys, x1 + major, ys);
2007
2008 /* grid line */
2009 if (grid != 0 && ys < y1 && ys > y1 - height)
2010 ctx.drawLine(x1, ys, x1 + grid, ys);
2011 }
2012
2013 if (logaxis) {
2014 dy *= 10;
2015 major_dy *= 10;
2016 label_dy *= 10;
2017 }
2018
2019 } else
2020 /* minor tick mark */
2021 ctx.drawLine(x1, ys, x1 + minor, ys);
2022
2023 /* for logaxis, also put labels on minor tick marks */
2024 if (logaxis) {
2025 if (label != 0) {
2026 str = y_act.toPrecision(n_sig1).stripZeros();
2027 ctx.save();
2028 ctx.fillStyle = "black";
2029 if (ys - textHeight / 2 > y1 - height &&
2030 ys + textHeight / 2 < y1 &&
2031 ys + textHeight < last_label_y + 2)
2032 ctx.fillText(str, x1 + label, ys);
2033 ctx.restore();
2034
2035 last_label_y = ys;
2036 }
2037 }
2038 }
2039
2040 y_act += dy;
2041
2042 /* supress 1.23E-17 ... */
2043 if (Math.abs(y_act) < dy / 100)
2044 y_act = 0;
2045
2046 } while (1);
2047}
2048
2049function mhttpd_resize_sidenav() {
2050 //console.log("mhttpd_resize_sidenav!");
2051 let h = document.getElementById('mheader');
2052 let s = document.getElementById('msidenav');
2053 let top = h.clientHeight + 1 + "px";
2054 if (s.style.top !== top) {
2055 //console.log("httpd_resize_sidenav: top changed from " + s.style.top + " to " + top);
2056 s.style.top = top;
2057 }
2058 let m = document.getElementById('mmain');
2059 let paddingTop = h.clientHeight + 1 + "px";
2060 if (m.style.paddingTop !== paddingTop) {
2061 //console.log("httpd_resize_sidenav: paddingTop changed from " + m.style.paddingTop + " to " + paddingTop);
2062 m.style.paddingTop = paddingTop;
2063 }
2064}
2065
2066function mhttpd_resize_header() {
2067
2068}
2069
2070let mhttpd_last_alarm = 0;
2071
2072function get_options_path(odb_path) {
2073 // Convert "/Path/to/something" to "/Path/to/Options something"
2074 let bits = odb_path.split("/");
2075 bits[bits.length - 1] = "Options " + bits[bits.length - 1];
2076 return bits.join("/");
2077}
2078
2079function evalPath(path) {
2080 let p = path;
2081 // if path is a function, evaluate it
2082 if (p && !p.startsWith('/') && p.includes('(') && p.includes(')'))
2083 p = eval(p);
2084 return p;
2085}
2086
2087function parseColorSpec(str) {
2088 if (typeof str !== "string")
2089 return "black";
2090
2091 // simple color
2092 if (!str.trim().startsWith('['))
2093 return str;
2094
2095 // convert string to valid JSON
2096 str = str
2097 .replace(/'/g, '"') // 'red' -> "red"
2098 .replace(/\{([^}]+)}/g, '[$1]'); // {0,"red"} → [0,"red"]
2099
2100 const pairs = JSON.parse(str);
2101
2102 str = pairs.map(([value, color]) => ({ value, color }));
2103
2104 return str;
2105}
2106
2107function mhttpd_refresh(boost) {
2108 if (!mhttpd_initialized)
2109 return;
2110
2111 if (mhttpd_refresh_id !== undefined)
2112 window.clearTimeout(mhttpd_refresh_id);
2113
2114 // don't do a refresh if we are paused
2115 if (mhttpd_refresh_paused) {
2116 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2117 return;
2118 }
2119
2120 // manage boost
2121 if (boost)
2122 mhttpd_refresh_boost = Date.now();
2123
2124 // don't update page if document is hidden (minimized, covered tab etc), only play alarms
2125 if (document.hidden) {
2126
2127 // only check every 10 seconds for alarm
2128 if (new Date().getTime() > mhttpd_last_alarm + 10000) {
2129
2130 // request current alarms
2131 let req = mjsonrpc_make_request("get_alarms");
2132 mjsonrpc_send_request([req]).then(function (rpc) {
2133
2134 let alarms = rpc[0].result;
2135
2136 // update alarm display
2137 if (alarms.alarm_system_active) {
2138 let n = 0;
2139 for (let a in alarms.alarms)
2140 if (alarms.alarms[a].alarm_sound)
2141 n++;
2142 if (n > 0)
2143 mhttpd_alarm_play();
2144 }
2145
2146 }).catch(function (error) {
2147 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2148 mhttpd_error('Connection to server broken. Trying to reconnect&nbsp;&nbsp;');
2149 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2150 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2151 } else {
2152 mjsonrpc_error_alert(error);
2153 }
2154 });
2155
2156 mhttpd_last_alarm = new Date().getTime();
2157 }
2158
2159 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, 500);
2160 return;
2161 }
2162
2163 // go through all modb* tags and refresh them -------------------------------------
2164
2165 let modb = getMElements("modb");
2166
2167 let modbvalue = getMElements("modbvalue");
2168 for (let i = 0; i < modbvalue.length; i++) {
2169
2170 let o = modbvalue[i];
2171 if (parseInt(o.dataset.input)) {
2172 if (o.childNodes[0] === undefined) {
2173 ODBInlineEdit(o, evalPath(o.dataset.odbPath), 0);
2174 }
2175 } else {
2176 if (parseInt(o.dataset.odbEditable)) {
2177 if (o.childNodes[0] === undefined || o.childNodes[0].tagName === undefined) {
2178
2179 // add link and event handler if tag is editable
2180 let link = document.createElement('a');
2181 link.href = "javascript:void(0);";
2182 link.onclick = function () {
2183 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2184 };
2185 link.onfocus = function () {
2186 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2187 };
2188
2189 o.innerHTML = "";
2190 o.appendChild(link);
2191 }
2192 } else {
2193
2194 // remove link if present
2195 if (o.childNodes[0] && o.childNodes[0].tagName === 'A')
2196 o.innerHTML = o.childNodes[0].innerHTML;
2197 }
2198 }
2199
2200 modbvalue[i].setValue = function (x, tid) {
2201 if (this.dataset.formula !== undefined)
2202 x = eval(this.dataset.formula);
2203 if (tid === undefined)
2204 tid = TID_DOUBLE;
2205 this.value = x;
2206 this.dataset.tid = tid;
2207
2208 if (this.onchange !== null)
2209 this.onchange();
2210 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2211 this.onload();
2212 this.dataset.odbLoaded = "1";
2213 }
2214 }
2215
2216 modbvalue[i].sendValueToOdb = function () {
2217 mhttpd_refresh_pause(true);
2218 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2219 mhttpd_refresh_pause(false));
2220 }
2221 }
2222
2223 let modbcheckbox = getMElements("modbcheckbox");
2224 for (let i = 0; i < modbcheckbox.length; i++) {
2225
2226 // add event listener if missing
2227 if (!modbcheckbox[i].mEventListener) {
2228 modbcheckbox[i].addEventListener("click", function () {
2229 if (this.dataset.validate !== undefined) {
2230 let flag = eval(this.dataset.validate)(this.checked, this);
2231 if (!flag) {
2232 mhttpd_refresh();
2233 return;
2234 }
2235 }
2236 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.checked ? 1 : 0).then(rpc =>
2237 mhttpd_refresh(true));
2238 });
2239 modbcheckbox[i].mEventListener = true;
2240 }
2241 }
2242
2243 let modbselect = getMElements("modbselect");
2244 for (let i = 0; i < modbselect.length; i++) {
2245 if (modbselect[i].dataset.odbPath) {
2246
2247 // add event listener if missing
2248 if (!modbselect[i].mEventListener) {
2249 modbselect[i].addEventListener("change", function (flag) {
2250
2251 if (flag !== true) { // flag === true if control changed by ODB update
2252 if (this.dataset.validate !== undefined) {
2253 let flag = eval(this.dataset.validate)(this.value, this);
2254 if (!flag) {
2255 mhttpd_refresh();
2256 return;
2257 }
2258 }
2259 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(rpc =>
2260 mhttpd_refresh(true));
2261 }
2262 });
2263 modbselect[i].mEventListener = true;
2264 }
2265 }
2266
2267 modbselect[i].setValue = function (x) {
2268 this.value = x;
2269
2270 if (this.onchange !== null)
2271 this.onchange();
2272 }
2273
2274 modbselect[i].sendValueToOdb = function () {
2275 mhttpd_refresh_pause(true);
2276 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2277 mhttpd_refresh_pause(false));
2278 }
2279
2280 }
2281
2282 let modbbox = getMElements("modbbox");
2283 for (let i = 0; i < modbbox.length; i++) {
2284 modbbox[i].style.border = "1px solid #808080";
2285 }
2286
2287 // attach "set" function to all ODB buttons
2288 let modbbutton = getMElements("modbbutton");
2289 for (let i = 0; i < modbbutton.length; i++) {
2290 if (!modbbutton[i].mEventListener) {
2291 modbbutton[i].addEventListener("click", function () {
2292
2293 if (this.dataset.validate !== undefined) {
2294 let flag = eval(this.dataset.validate)(this);
2295 if (!flag) {
2296 mhttpd_refresh();
2297 return;
2298 }
2299 }
2300
2301 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.dataset.odbValue).then(rpc =>
2302 mhttpd_refresh(true));
2303 });
2304 modbbutton[i].mEventListener = true;
2305 }
2306 }
2307
2308 // replace all horizontal bars with proper <div>'s
2309 let modbhbar = getMElements("modbhbar");
2310 for (let i = 0; i < modbhbar.length; i++) {
2311 if (modbhbar[i].childNodes === undefined || modbhbar[i].childNodes[0] === undefined ||
2312 modbhbar[i].childNodes[0].tagName !== 'DIV') {
2313 modbhbar[i].style.display = "block";
2314 if (modbhbar[i].style.position === "")
2315 modbhbar[i].style.position = "relative";
2316 modbhbar[i].style.border = "1px solid #808080";
2317 let color = modbhbar[i].style.color;
2318
2319 modbhbar[i].innerHTML = "<div style='background-color:" + color + ";" + "color:black;" +
2320 "width:0;height:100%; " +
2321 "position:relative; display:inline-block;border-right:1px solid #808080'>&nbsp;</div>";
2322
2323 modbhbar[i].setValue = function (x, tid) {
2324 if (this.dataset.formula !== undefined)
2325 x = eval(this.dataset.formula);
2326 if (tid === undefined)
2327 tid = TID_DOUBLE;
2328 let mvalue = mie_to_string(tid, x, this.dataset.format);
2329 if (mvalue === "")
2330 mvalue = "(empty)";
2331 let html = mhttpd_escape("&nbsp;" + mvalue);
2332 this.value = x;
2333 if (this.dataset.printValue === "1")
2334 this.children[0].innerHTML = html;
2335 let minValue = parseFloat(this.dataset.minValue);
2336 let maxValue = parseFloat(this.dataset.maxValue);
2337 if (isNaN(minValue))
2338 minValue = 0;
2339 if (this.dataset.log === "1" &&
2340 minValue === 0)
2341 minValue = 1E-3;
2342 if (isNaN(maxValue))
2343 maxValue = 1;
2344 let percent;
2345 if (this.dataset.log === "1")
2346 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2347 (Math.log(maxValue) - Math.log(minValue)));
2348 else
2349 percent = Math.round(100 * (x - minValue) /
2350 (maxValue - minValue));
2351 if (percent < 0)
2352 percent = 0;
2353 if (percent > 100)
2354 percent = 100;
2355 this.children[0].style.width = percent + "%";
2356 if (this.onchange !== null)
2357 this.onchange();
2358 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2359 this.onload();
2360 this.dataset.odbLoaded = "1";
2361 }
2362 this.children[0].style.backgroundColor = this.style.color;
2363 }
2364 }
2365 }
2366
2367 // replace all vertical bars with proper <div>'s
2368 let modbvbar = getMElements("modbvbar");
2369 for (let i = 0; i < modbvbar.length; i++) {
2370 if (modbvbar[i].childNodes === undefined || modbvbar[i].childNodes[0] === undefined ||
2371 modbvbar[i].childNodes[0].tagName !== 'DIV') {
2372 modbvbar[i].style.display = "inline-block";
2373 if (modbvbar[i].style.position === "")
2374 modbvbar[i].style.position = "relative";
2375 modbvbar[i].style.border = "1px solid #808080";
2376 let color = modbvbar[i].style.color;
2377 modbvbar[i].innerHTML = "<div style='background-color:" + color + "; height:0; width:100%; " +
2378 "position:absolute; bottom:0; left:0; display:inline-block; border-top:1px solid #808080'>&nbsp;</div>";
2379
2380 modbvbar[i].setValue = function (x, tid) {
2381 if (this.dataset.formula !== undefined)
2382 x = eval(this.dataset.formula);
2383 if (tid === undefined)
2384 tid = TID_DOUBLE;
2385 let mvalue = mie_to_string(tid, x, this.dataset.format);
2386 if (mvalue === "")
2387 mvalue = "(empty)";
2388 let html = mhttpd_escape("&nbsp;" + mvalue);
2389 this.value = x;
2390 if (this.dataset.printValue === "1")
2391 this.children[0].innerHTML = html;
2392 let minValue = parseFloat(this.dataset.minValue);
2393 let maxValue = parseFloat(this.dataset.maxValue);
2394 if (isNaN(minValue))
2395 minValue = 0;
2396 if (this.dataset.log === "1" &&
2397 minValue === 0)
2398 minValue = 1E-3;
2399 if (isNaN(maxValue))
2400 maxValue = 1;
2401 let percent;
2402 if (this.dataset.log === "1")
2403 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2404 (Math.log(maxValue) - Math.log(minValue)));
2405 else
2406 percent = Math.round(100 * (x - minValue) /
2407 (maxValue - minValue));
2408 if (percent < 0)
2409 percent = 0;
2410 if (percent > 100)
2411 percent = 100;
2412 this.children[0].style.height = percent + "%";
2413 if (this.onchange !== null)
2414 this.onchange();
2415 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2416 this.onload();
2417 this.dataset.odbLoaded = "1";
2418 }
2419 this.children[0].style.backgroundColor = this.style.color;
2420 }
2421 }
2422 }
2423
2424 // replace all thermometers with canvas
2425 let modbthermo = getMElements("modbthermo");
2426 for (let i = 0; i < modbthermo.length; i++) {
2427 if (modbthermo[i].childNodes === undefined || modbthermo[i].childNodes[0] === undefined ||
2428 modbthermo[i].childNodes[0].tagName !== 'CANVAS') {
2429 modbthermo[i].style.display = "inline-block";
2430 if (modbthermo[i].style.position === "")
2431 modbthermo[i].style.position = "relative";
2432
2433 cvs = document.createElement("canvas");
2434 if (modbthermo[i].style.width === "") {
2435 dlgAlert("modbthermo #" + i +": undefined width");
2436 return;
2437 }
2438 if (modbthermo[i].style.height === "") {
2439 dlgAlert("modbthermo #" + i +": undefined height");
2440 return;
2441 }
2442 let w = Number.parseInt(modbthermo[i].style.width);
2443 let h = Number.parseInt(modbthermo[i].style.height);
2444
2445 w = Math.floor(w / 4) * 4; // 2 must be dividable by 4
2446 cvs.width = w + 1;
2447 cvs.height = h;
2448 modbthermo[i].appendChild(cvs);
2449 modbthermo[i].draw = mhttpd_thermo_draw;
2450
2451 modbthermo[i].setValue = function (x, tid) {
2452 if (this.dataset.formula !== undefined)
2453 x = eval(this.dataset.formula);
2454 if (tid === undefined)
2455 tid = TID_DOUBLE;
2456 this.value = x;
2457 this.dataset.tid = tid;
2458
2459 if (this.onchange !== null)
2460 this.onchange();
2461 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2462 this.onload();
2463 this.dataset.odbLoaded = "1";
2464 }
2465
2466 this.draw();
2467 }
2468
2469 modbthermo[i].draw();
2470 }
2471 }
2472
2473 // replace all gauges with canvas
2474 let modbgauge = getMElements("modbgauge");
2475 for (let i = 0; i < modbgauge.length; i++) {
2476 if (modbgauge[i].childNodes === undefined || modbgauge[i].childNodes[0] === undefined ||
2477 modbgauge[i].childNodes[0].tagName !== 'CANVAS') {
2478 modbgauge[i].style.display = "inline-block";
2479 if (modbgauge[i].style.position === "")
2480 modbgauge[i].style.position = "relative";
2481
2482 let cvs = document.createElement("canvas");
2483 if (modbgauge[i].style.width === "") {
2484 dlgAlert("modbgauge #" + i +": undefined width");
2485 return;
2486 }
2487 if (modbgauge[i].style.height === "") {
2488 dlgAlert("modbgauge #" + i +": undefined height");
2489 return;
2490 }
2491
2492 cvs.width = Number.parseInt(modbgauge[i].style.width);
2493 cvs.height = Number.parseInt(modbgauge[i].style.height);
2494 modbgauge[i].appendChild(cvs);
2495 modbgauge[i].draw = mhttpd_gauge_draw;
2496
2497 modbgauge[i].setValue = function (x, tid) {
2498 if (this.dataset.formula !== undefined)
2499 x = eval(this.dataset.formula);
2500 if (tid === undefined)
2501 tid = TID_DOUBLE;
2502 this.value = x;
2503 this.dataset.tid = tid;
2504
2505 if (this.onchange !== null)
2506 this.onchange();
2507 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2508 this.onload();
2509 this.dataset.odbLoaded = "1";
2510 }
2511
2512 this.draw();
2513 }
2514
2515 modbgauge[i].draw();
2516 }
2517 }
2518
2519 // replace all haxis with canvas
2520 let mhaxis = getMElements("mhaxis");
2521 for (let i = 0; i < mhaxis.length; i++) {
2522 if (mhaxis[i].childNodes === undefined || mhaxis[i].childNodes[0] === undefined ||
2523 mhaxis[i].childNodes[0].tagName !== 'CANVAS') {
2524 mhaxis[i].style.display = "block";
2525 if (mhaxis[i].style.position === "")
2526 mhaxis[i].style.position = "relative";
2527
2528 let cvs = document.createElement("canvas");
2529 if (mhaxis[i].style.width === "") {
2530 dlgAlert("mhaxis #" + i +": undefined width");
2531 return;
2532 }
2533 if (mhaxis[i].style.height === "") {
2534 dlgAlert("mhaxis #" + i +": undefined height");
2535 return;
2536 }
2537 cvs.width = Number.parseInt(mhaxis[i].style.width) + 2;
2538 cvs.height = Number.parseInt(mhaxis[i].style.height);
2539 mhaxis[i].appendChild(cvs);
2540 mhaxis[i].draw = mhttpd_haxis_draw;
2541 mhaxis[i].draw();
2542 }
2543 }
2544
2545 // replace all vaxis with canvas
2546 let mvaxis = getMElements("mvaxis");
2547 for (let i = 0; i < mvaxis.length; i++) {
2548 if (mvaxis[i].childNodes === undefined || mvaxis[i].childNodes[0] === undefined ||
2549 mvaxis[i].childNodes[0].tagName !== 'CANVAS') {
2550 mvaxis[i].style.display = "inline-block";
2551 if (mvaxis[i].style.position === "")
2552 mvaxis[i].style.position = "relative";
2553
2554 let cvs = document.createElement("canvas");
2555 if (mvaxis[i].style.width === "") {
2556 dlgAlert("mvaxis #" + i +": undefined width");
2557 return;
2558 }
2559 if (mvaxis[i].style.height === "") {
2560 dlgAlert("mvaxis #" + i +": undefined height");
2561 return;
2562 }
2563 cvs.width = Number.parseInt(mvaxis[i].style.width);
2564 cvs.height = Number.parseInt(mvaxis[i].style.height) + 2;
2565 mvaxis[i].appendChild(cvs);
2566 mvaxis[i].draw = mhttpd_vaxis_draw;
2567 mvaxis[i].draw();
2568 }
2569 }
2570
2571 // replace all mhistory tags with history plots
2572 let mhist = getMElements("mhistory");
2573 for (let i = 0; i < mhist.length; i++) {
2574 if (mhist[i].childNodes === undefined || mhist[i].childNodes[0] === undefined ||
2575 mhist[i].childNodes[0].tagName !== 'IMG') {
2576 let w = mhist[i].style.width;
2577 if (w === "")
2578 w = 320;
2579 else
2580 w = parseInt(w);
2581 let h = mhist[i].style.height;
2582 if (h === "")
2583 h = 200;
2584 else
2585 h = parseInt(h);
2586 mhist[i].innerHTML = "<img src=\"graph.gif?cmd=oldhistory&group=" +
2587 mhist[i].dataset.group +
2588 "&panel=" + mhist[i].dataset.panel +
2589 "&scale=" + mhist[i].dataset.scale +
2590 "&width=" + w +
2591 "&height=" + h +
2592 "&rnd=" + (new Date().getTime()) +
2593 "\">";
2594 }
2595 }
2596
2597 // request all modb* elements
2598 let paths = [];
2599 let modbs = ["modb", "modbvalue", "modbcheckbox", "modbselect", "modbbox", "modbhbar",
2600 "modbvbar", "modbthermo", "modbgauge"];
2601
2602 modbs.forEach((mstr) => {
2603 getMElements(mstr).forEach((m) => {
2604 let p = m.dataset.odbPath;
2605 if (p) {
2606 p = evalPath(p);
2607 if (p.includes('[*]'))
2608 p = p.replace('[*]', '');
2609 paths.push(p);
2610 }
2611
2612 if (mstr === "modbselect" && m.dataset.autoOptions === "1")
2613 paths.push(get_options_path(evalPath(m.dataset.odbPath)));
2614
2615 })
2616 });
2617
2618 // request ODB contents for all variables
2619 let req1 = mjsonrpc_make_request("db_get_values", {"paths": paths});
2620
2621 // request current alarms
2622 let req2 = mjsonrpc_make_request("get_alarms");
2623
2624 // request new messages
2625 let req3 = mjsonrpc_make_request("cm_msg_retrieve", {
2626 "facility": "midas",
2627 "time": 0,
2628 "min_messages": 1
2629 });
2630
2631 // request new char messages
2632 let req4 = mjsonrpc_make_request("cm_msg_retrieve", {
2633 "facility": "chat",
2634 "time": 0,
2635 "min_messages": 1
2636 });
2637
2638 mjsonrpc_send_request([req1, req2, req3, req4]).then(function (rpc) {
2639
2640 // update time in header
2641 let dstr = mhttpd_get_display_time().string;
2642
2643 if (document.getElementById("mheader_last_updated") !== undefined) {
2644 //mhttpd_set_innerHTML(document.getElementById("mheader_last_updated"), dstr);
2645 mhttpd_set_firstChild_data(document.getElementById("mheader_last_updated"), dstr);
2646 }
2647
2648 let idata = 0;
2649
2650 for (let i = 0; i < modb.length; i++, idata++) {
2651 if (modb[i].dataset.odbPath === undefined) {
2652 idata--;
2653 continue;
2654 }
2655 let x = rpc[0].result.data[idata];
2656 if (typeof x === 'object' && x !== null) {
2657 // subdircectory
2658 if (modb[i].json_value === undefined) {
2659 modb[i].json_value = JSON.stringify(x);
2660 modb[i].value = x;
2661 }
2662 if (modb[i].onchange !== null && JSON.stringify(x) !== modb[i].json_value) {
2663 modb[i].json_value = JSON.stringify(x);
2664 modb[i].value = x;
2665 modb[i].onchange();
2666 }
2667 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2668 modb[i].onload();
2669 modb[i].dataset.odbLoaded = "1";
2670 }
2671 } else {
2672 // individual value
2673 if (modb[i].value === undefined)
2674 modb[i].value = x;
2675 if (modb[i].onchange !== null && x !== modb[i].value) {
2676 modb[i].value = x;
2677 modb[i].onchange();
2678 }
2679 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2680 modb[i].onload();
2681 modb[i].dataset.odbLoaded = "1";
2682 }
2683 }
2684 }
2685
2686 for (let i = 0; i < modbvalue.length; i++, idata++) {
2687 if (modbvalue[i].dataset.odbPath === undefined) {
2688 idata--;
2689 continue;
2690 }
2691
2692 if (rpc[0].result.status[idata] === 312) {
2693 modbvalue[i].innerHTML = "ODB key \"" +
2694 evalPath(modbvalue[i].dataset.odbPath) + "\" not found";
2695 } else {
2696 let x = rpc[0].result.data[idata];
2697 let tid = rpc[0].result.tid[idata];
2698
2699 modbvalue[i].tid = tid;
2700 if (modbvalue[i].dataset.formula !== undefined)
2701 x = eval(modbvalue[i].dataset.formula);
2702 let mvalue = mie_to_string(tid, x, modbvalue[i].dataset.format);
2703 if (mvalue === "")
2704 mvalue = "(empty)";
2705 else if (typeof mvalue === 'string' && mvalue.trim() === "")
2706 mvalue = "(spaces)";
2707 let html;
2708 if (tid === TID_BOOL)
2709 html = mvalue;
2710 else
2711 html = mhttpd_escape(mvalue);
2712 if (parseInt(modbvalue[i].dataset.odbEditable) || parseInt(modbvalue[i].dataset.input)) {
2713 if (modbvalue[i].childNodes[1] !== undefined &&
2714 modbvalue[i].childNodes[1].innerHTML !== undefined) {
2715 modbvalue[i].childNodes[1].innerHTML = html;
2716 } else {
2717 if (modbvalue[i].childNodes[0]) // dataset.input could not yet be initialized (AJAX)
2718 modbvalue[i].childNodes[0].innerHTML = html;
2719 }
2720 } else
2721 modbvalue[i].innerHTML = html;
2722
2723 if (modbvalue[i].value !== x && modbvalue[i].onchange !== null) {
2724 modbvalue[i].value = x;
2725 if (modbvalue[i].value !== undefined)
2726 modbvalue[i].onchange();
2727 }
2728
2729 modbvalue[i].value = x;
2730
2731 if (modbvalue[i].dataset.odbLoaded === undefined && modbvalue[i].onload !== null) {
2732 modbvalue[i].onload();
2733 modbvalue[i].dataset.odbLoaded = "1";
2734 }
2735 }
2736 }
2737
2738 for (let i = 0; i < modbcheckbox.length; i++, idata++) {
2739 if (modbcheckbox[i].dataset.odbPath === undefined) {
2740 idata--;
2741 continue;
2742 }
2743
2744 let x = rpc[0].result.data[idata];
2745 let tid = rpc[0].result.tid[idata];
2746 let mvalue = mie_to_string(tid, x);
2747 if (typeof x === "boolean")
2748 modbcheckbox[i].checked = x;
2749 else
2750 modbcheckbox[i].checked = (mvalue !== "0");
2751 if (modbcheckbox[i].onchange !== null)
2752 modbcheckbox[i].onchange();
2753 if (modbcheckbox[i].dataset.odbLoaded === undefined && modbcheckbox[i].onload !== null) {
2754 modbcheckbox[i].onload();
2755 modbcheckbox[i].dataset.odbLoaded = "1";
2756 }
2757 }
2758
2759 for (let i = 0; i < modbselect.length; i++, idata++) {
2760 if (modbselect[i].dataset.odbPath === undefined) {
2761 idata--;
2762 continue;
2763 }
2764
2765 let x = rpc[0].result.data[idata];
2766 let auto_options = [];
2767
2768 if (modbselect[i].dataset.autoOptions == "1") {
2769 idata++;
2770 auto_options = rpc[0].result.data[idata];
2771
2772 if (auto_options !== null && modbselect[i].options.length == 0) {
2773 for (let opt = 0; opt < auto_options.length; opt++) {
2774 modbselect[i].add(new Option(auto_options[opt], auto_options[opt]));
2775 }
2776 }
2777 }
2778
2779 if (rpc[0].result.status[idata] === 312) {
2780 modbselect[i].parentElement.innerHTML = "ODB key \"" +
2781 evalPath(modbselect[i].dataset.odbPath) + "\" not found";
2782 } else {
2783 if (x !== null && modbselect[i].value !== x.toString()) {
2784
2785 if (modbselect[i].value !== undefined) {
2786 // check if option is available
2787 let j;
2788 for (j = 0; j < modbselect[i].options.length; j++)
2789 if (modbselect[i].options[j].value === x.toString())
2790 break;
2791
2792 // if option is available, set it
2793 if (j < modbselect[i].options.length) {
2794 modbselect[i].value = x.toString();
2795 if (modbselect[i].onchange !== null)
2796 modbselect[i].onchange(true);
2797 } else {
2798 // if not, set blank
2799 if (modbselect[i].value !== "")
2800 modbselect[i].value = "";
2801 }
2802 }
2803
2804 if (Array.isArray(x)) {
2805 // check if all array values are the same
2806 let s = new Set(x);
2807 if (s.size === 1)
2808 modbselect[i].value = s.values().next().value;
2809 else
2810 modbselect[i].value = "...";
2811 }
2812 }
2813 if (modbselect[i].dataset.odbLoaded === undefined && modbselect[i].onload !== null) {
2814 modbselect[i].onload();
2815 modbselect[i].dataset.odbLoaded = "1";
2816 }
2817 }
2818 }
2819
2820 for (let i = 0; i < modbbox.length; i++, idata++) {
2821 if (modbbox[i].dataset.odbPath === undefined) {
2822 idata--;
2823 continue;
2824 }
2825
2826 let x = rpc[0].result.data[idata];
2827 if (modbbox[i].dataset.formula !== undefined)
2828 x = eval(modbbox[i].dataset.formula);
2829 if (x > 0 || x === true) {
2830 modbbox[i].style.backgroundColor = modbbox[i].dataset.color;
2831 } else {
2832 if (modbbox[i].dataset.backgroundColor !== undefined)
2833 modbbox[i].style.backgroundColor = modbbox[i].dataset.backgroundColor;
2834 else
2835 modbbox[i].style.backgroundColor = "";
2836 }
2837 if (modbbox[i].onchange !== null)
2838 modbbox[i].onchange();
2839 if (modbbox[i].dataset.odbLoaded === undefined && modbbox[i].onload !== null) {
2840 modbbox[i].onload();
2841 modbbox[i].dataset.odbLoaded = "1";
2842 }
2843 }
2844
2845 for (let i = 0; i < modbhbar.length; i++, idata++) {
2846 if (modbhbar[i].dataset.odbPath === undefined) {
2847 idata--;
2848 continue;
2849 }
2850
2851 let x = rpc[0].result.data[idata];
2852 let tid = rpc[0].result.tid[idata];
2853
2854 modbhbar[i].setValue(x, tid);
2855 }
2856
2857 for (let i = 0; i < modbvbar.length; i++, idata++) {
2858 if (modbvbar[i].dataset.odbPath === undefined) {
2859 idata--;
2860 continue;
2861 }
2862
2863 let x = rpc[0].result.data[idata];
2864 let tid = rpc[0].result.tid[idata];
2865
2866 modbvbar[i].setValue(x, tid);
2867 }
2868
2869 for (let i = 0; i < modbthermo.length; i++, idata++) {
2870 if (modbthermo[i].dataset.odbPath === undefined) {
2871 idata--;
2872 continue;
2873 }
2874
2875 let x = rpc[0].result.data[idata];
2876 let tid = rpc[0].result.tid[idata];
2877
2878 modbthermo[i].setValue(x, tid);
2879 }
2880
2881 for (let i = 0; i < modbgauge.length; i++, idata++) {
2882 if (modbgauge[i].dataset.odbPath === undefined) {
2883 idata--;
2884 continue;
2885 }
2886
2887 let x = rpc[0].result.data[idata];
2888 let tid = rpc[0].result.tid[idata];
2889
2890 modbgauge[i].setValue(x, tid);
2891 }
2892
2893 let alarms = rpc[1].result;
2894
2895 // update alarm display
2896 let e = document.getElementById('mheader_alarm');
2897 if (!alarms.alarm_system_active) {
2898 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: Off</a>");
2899 mhttpd_set_className(e, "mgray mbox");
2900 } else {
2901 let s = "";
2902 let n = 0;
2903 let sound = 0;
2904 for (let a in alarms.alarms) {
2905 s += a + ", ";
2906 n++;
2907
2908 if (alarms.alarms[a].alarm_sound)
2909 sound++;
2910 }
2911 if (n < 1) {
2912 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: None</a>");
2913 mhttpd_set_className(e, "mgreen mbox");
2914 } else {
2915 s = s.slice(0, -2);
2916 if (n > 1)
2917 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: " + s + "</a>");
2918 else
2919 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarm: " + s + "</a>");
2920 mhttpd_set_className(e, "mred mbox");
2921
2922 if (sound > 0)
2923 mhttpd_alarm_play();
2924 }
2925 }
2926
2927 // update messages
2928 let msg;
2929 if (rpc[2].result.messages !== undefined) {
2930 msg = rpc[2].result.messages.split("\n");
2931 if (msg[msg.length - 1] === "")
2932 msg = msg.slice(0, -1);
2933 } else
2934 msg = undefined;
2935
2936 // update chat messages
2937 let chat;
2938 if (rpc[3].result.messages !== undefined) {
2939 chat = rpc[3].result.messages.split("\n");
2940 if (chat[chat.length - 1] === "")
2941 chat = chat.slice(0, -1);
2942 } else
2943 chat = undefined;
2944
2945 mhttpd_message(msg, chat);
2946 mhttpd_resize_sidenav();
2947
2948 if (mhttpd_refresh_interval !== undefined && mhttpd_refresh_interval > 0) {
2949 // use 100ms refresh period in boost mode (500 ms after last write)
2950 if (Date.now() < mhttpd_refresh_boost + 500)
2951 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, 100);
2952 else
2953 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2954 }
2955
2956 }).catch(function (error) {
2957
2958 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2959 mhttpd_error('Connection to server broken. Trying to reconnect&nbsp;&nbsp;');
2960 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2961 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2962 } else {
2963 mjsonrpc_error_alert(error);
2964 }
2965 });
2966}
2967
2968function mhttpd_refresh_history() {
2969 if (mhttpd_refresh_history_id !== undefined)
2970 window.clearTimeout(mhttpd_refresh_history_id);
2971
2972 /* this fuction gets called by mhttpd_init to periodically refresh all history panels */
2973
2974 let mhist = document.getElementsByName("mhistory");
2975 for (let i = 0; i < mhist.length; i++) {
2976 let s = mhist[i].childNodes[0].src;
2977
2978 if (s.lastIndexOf("&rnd=") !== -1) {
2979 s = s.substr(0, s.lastIndexOf('&rnd='));
2980 s += "&rnd=" + new Date().getTime();
2981 }
2982 mhist[i].childNodes[0].src = s;
2983 }
2984
2985 if (mhttpd_refresh_history_interval !== undefined && mhttpd_refresh_history_interval > 0)
2986 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2987}
2988
2989function mhttpd_reconnect() {
2990 mjsonrpc_db_ls(["/"]).then(function (rpc) {
2991 // on successful connection remove error and schedule refresh
2992 mhttpd_error_clear();
2993 if (mhttpd_refresh_id !== undefined)
2994 window.clearTimeout(mhttpd_refresh_id);
2995 if (mhttpd_refresh_history_id !== undefined)
2996 window.clearTimeout(mhttpd_refresh_history_id);
2997 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2998 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2999 }).catch(function (error) {
3000 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
3001 });
3002}
3003
3004window.addEventListener('resize', mhttpd_resize_message);
3005
3006function mhttpd_resize_message() {
3007 //console.log("mhttpd_resize_message() via resize event listener");
3008 let d = document.getElementById("mheader_message");
3009 if (d.currentMessage !== undefined && d.style.display !== 'none')
3010 mhttpd_fit_message(d.currentMessage);
3011}
3012
3013function mhttpd_close_message() {
3014 let d = document.getElementById("mheader_message");
3015
3016 // remember time of messages to suppress
3017 mhttpdConfigSet('suppressMessageBefore', d.currentMessageT);
3018
3019 d.style.display = "none";
3020 mhttpd_resize_sidenav();
3021}
3022
3023function mhttpd_fit_message(m) {
3024 let d = document.getElementById("mheader_message");
3025 let cross = "&nbsp;&nbsp;&nbsp;<span style=\"cursor: pointer;\" onclick=\"mhttpd_close_message();\">&#9587;</span>";
3026 let link1 = "<span style=\"cursor: pointer;\" onclick=\"window.location.href='?cmd=Messages'\">";
3027 let link2 = "</span>";
3028 d.style.display = "inline-block";
3029
3030 // limit message to fit parent element
3031
3032 let parentWidth = d.parentNode.offsetWidth;
3033 let maxWidth = parentWidth - 30;
3034
3035 // check if the full message fits
3036
3037 d.innerHTML = link1 + m + link2 + cross;
3038 //console.log("mhttpd_fit_message: len: " + d.offsetWidth + ", max: " + maxWidth + ", message: " + m);
3039 if (d.offsetWidth <= maxWidth) {
3040 return;
3041 }
3042
3043 // check if the message minus timestamp and type fits
3044
3045 m = m.substr(m.indexOf(']')+1);
3046 d.innerHTML = link1 + m + link2 + cross;
3047 let w = d.offsetWidth;
3048 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + m);
3049 if (w <= maxWidth) {
3050 return;
3051 }
3052
3053 // guess the length assuming fix pixels per char
3054
3055 let charWidth = w/m.length;
3056 let guessLength = maxWidth/charWidth - 3; // 3 chars of "..."
3057
3058 let g = m.substr(0, guessLength);
3059 d.innerHTML = link1 + g + "..." + link2 + cross;
3060 w = d.offsetWidth;
3061 //console.log("mhttpd_fit_message: char: " + charWidth + ", guess: " + guessLength + ", len: " + w + ", max: " + maxWidth);
3062
3063 // grow or shrink our guess
3064
3065 if (w < maxWidth) {
3066 //console.log("mhttpd_fit_message: too short, grow");
3067 for (let i=guessLength+1; i<=m.length; i++) {
3068 let s = m.substr(0, i);
3069 d.innerHTML = link1 + s + "..." + link2 + cross;
3070 w = d.offsetWidth;
3071 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + s);
3072 if (w <= maxWidth)
3073 break;
3074 }
3075 } else {
3076 //console.log("mhttpd_fit_message: too long, shrink");
3077 while (g.length > 0) {
3078 g = g.substr(0, g.length-1);
3079 d.innerHTML = link1 + g + "..." + link2 + cross;
3080 w = d.offsetWidth;
3081 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + g);
3082 if (w <= maxWidth)
3083 break;
3084 }
3085 }
3086}
3087
3088function mhttpd_message(msg, chat) {
3089
3090 let mTalk = "";
3091 let mType = "";
3092 let chatName = "";
3093 let talkTime = 0;
3094 let notifyTime = 0;
3095 let lastMsg = "";
3096 let lastMsgT = 0;
3097 let lastChat = "";
3098 let lastChatT = 0;
3099 let lastT = 0;
3100 let m = "";
3101 let c = "";
3102
3103 if (msg !== undefined) {
3104 lastMsg = msg[0].substr(msg[0].indexOf(" ") + 1);
3105 lastMsgT = parseInt(msg[0]);
3106 }
3107
3108 if (chat !== undefined) {
3109 lastChat = chat[0].substr(chat[0].indexOf(" ") + 1);
3110 lastChatT = parseInt(chat[0]);
3111 if (chat[0].length > 0)
3112 mTalk = chat[0].substr(chat[0].indexOf("]") + 2);
3113
3114 chatName = lastChat.substring(lastChat.indexOf("[") + 1, lastChat.indexOf(","));
3115 lastChat = lastChat.substr(0, lastChat.indexOf("[")) +
3116 "<b>" + chatName + ":</b>" +
3117 lastChat.substr(lastChat.indexOf("]") + 1);
3118 }
3119
3120 if (lastChatT > lastMsgT) {
3121 m = lastChat;
3122 c = "var(--mblue)";
3123 mType = "USER";
3124 talkTime = lastChatT;
3125 notifyTime = lastChatT;
3126 lastT = lastChatT;
3127 } else {
3128 m = lastMsg;
3129 c = "var(--myellow)";
3130 mTalk = lastMsg.substr(lastMsg.indexOf("]") + 1);
3131 mType = m.substring(m.indexOf(",") + 1, m.indexOf("]"));
3132 talkTime = lastMsgT;
3133 notifyTime = lastMsgT;
3134 lastT = lastMsgT;
3135 }
3136
3137 if (m !== "") {
3138 let d = document.getElementById("mheader_message");
3139 if (d !== undefined && d !== null && d.currentMessage !== m &&
3140 (mhttpdConfig().suppressMessageBefore === undefined || lastT > mhttpdConfig().suppressMessageBefore)) {
3141
3142 d.style.removeProperty("-webkit-transition");
3143 d.style.removeProperty("transition");
3144
3145 if (mType === "USER" && mhttpdConfig().displayChat ||
3146 mType === "TALK" && mhttpdConfig().displayTalk ||
3147 mType === "ERROR" && mhttpdConfig().displayError ||
3148 mType === "INFO" && mhttpdConfig().displayInfo ||
3149 mType === "LOG" && mhttpdConfig().displayLog) {
3150
3151 let first = (d.currentMessage === undefined);
3152 d.currentMessage = m; // store full message in user-defined attribute
3153 d.currentMessageT = lastMsgT; // store message time in user-defined attribute
3154
3155 mhttpd_fit_message(m);
3156 d.age = new Date() / 1000;
3157
3158 if (first) {
3159 if (m.search("ERROR]") > 0) {
3160 d.style.backgroundColor = "var(--mred)";
3161 d.style.color = "white";
3162 }
3163 } else {
3164
3165 // manage backgroud color (red for errors, fading yellow for others)
3166 if (m.search("ERROR]") > 0) {
3167 d.style.removeProperty("-webkit-transition");
3168 d.style.removeProperty("transition");
3169 d.style.backgroundColor = "var(--mred)";
3170 } else {
3171 d.age = new Date() / 1000;
3172 d.style.removeProperty("-webkit-transition");
3173 d.style.removeProperty("transition");
3174 d.style.backgroundColor = c;
3175 }
3176 }
3177 }
3178
3179 if (mTalk !== "") {
3180 if (mType === "USER" && mhttpdConfig().speakChat) {
3181 // do not speak own message
3182 if (document.getElementById("chatName") === undefined ||
3183 document.getElementById("chatName") === null ||
3184 document.getElementById("chatName").value !== chatName) {
3185 mhttpd_speak(talkTime, mTalk);
3186 }
3187 } else if (mType === "TALK" && mhttpdConfig().speakTalk) {
3188 mhttpd_speak(talkTime, mTalk);
3189 } else if (mType === "ERROR" && mhttpdConfig().speakError) {
3190 mhttpd_speak(talkTime, mTalk);
3191 } else if (mType === "INFO" && mhttpdConfig().speakInfo) {
3192 mhttpd_speak(talkTime, mTalk);
3193 } else if (mType === "LOG" && mhttpdConfig().speakLog) {
3194 mhttpd_speak(talkTime, mTalk);
3195 }
3196 }
3197
3198 if (mType === "USER" && mhttpdConfig().notifyChat ||
3199 mType === "TALK" && mhttpdConfig().notifyTalk ||
3200 mType === "ERROR" && mhttpdConfig().notifyError ||
3201 mType === "INFO" && mhttpdConfig().notifyInfo ||
3202 mType === "LOG" && mhttpdConfig().notifyLog) {
3203
3204 // avoid double notification if page gets reloaded
3205 // console.log("notifyTime: " + notifyTime + " last: " + mhttpdConfig().var.lastNotify);
3206 if (mhttpdConfig().var.lastNotify === undefined || notifyTime > mhttpdConfig().var.lastNotify) {
3207 mhttpdConfigSet("var.lastNotify", notifyTime);
3208
3209 if (("Notification" in window)) {
3210 if (Notification.permission === "granted") {
3211 const n = new Notification("MIDAS", {
3212 body: mTalk,
3213 icon: "./apple-touch-icon.png"
3214 });
3215 } else if (Notification.permission !== "denied") {
3216 Notification.requestPermission().then((p) => {
3217 if (p === "granted") {
3218 const n = new Notification("MIDAS", {
3219 body: mTalk,
3220 icon: "./apple-touch-icon.png"
3221 });
3222 }
3223 });
3224 }
3225 }
3226 }
3227 }
3228
3229
3230 }
3231 let t = new Date() / 1000;
3232 if (t > d.age + 5 && d.style.backgroundColor === "var(--myellow)") {
3233 let backgroundColor = "var(--mgray)";
3234 d.style.setProperty("-webkit-transition", "background-color 3s", "");
3235 d.style.setProperty("transition", "background-color 3s", "");
3236 d.style.backgroundColor = backgroundColor;
3237 }
3238 }
3239}
3240
3241function mhttpd_error(error) {
3242 let d = document.getElementById("mheader_error");
3243 if (d !== undefined) {
3244 error += "<div style=\"display: inline; float: right; padding-right: 10px; cursor: pointer;\"" +
3245 " onclick=\"document.getElementById(&quot;mheader_error&quot;).style.zIndex = 0;\">&#9587;</div>";
3246 d.innerHTML = error;
3247 d.style.zIndex = 3; // above header
3248 }
3249}
3250
3251function mhttpd_error_clear() {
3252 if (document.getElementById("mheader_error")) {
3253 document.getElementById("mheader_error").innerHTML = "";
3254 document.getElementById("mheader_error").style.zIndex = 0; // below header
3255 }
3256}
3257
3258function mhttpd_create_page_handle_create(mouseEvent) {
3259 let path = "";
3260 let type = "";
3261 let name = "";
3262 let arraylength = "";
3263 let stringlength = "";
3264
3265 let form = document.getElementsByTagName('form')[0];
3266
3267 if (form) {
3268 path = form.elements['odb'].value;
3269 type = form.elements['type'].value;
3270 name = form.elements['value'].value;
3271 arraylength = form.elements['index'].value;
3272 stringlength = form.elements['strlen'].value;
3273 } else {
3274 let e = document.getElementById("odbpath");
3275 path = JSON.parse(e.innerHTML);
3276 if (path === "/") path = "";
3277
3278 type = document.getElementById("create_tid").value;
3279 name = document.getElementById("create_name").value;
3280 arraylength = document.getElementById("create_array_length").value;
3281 stringlength = document.getElementById("create_strlen").value;
3282
3283 //alert("Path: " + path + " Name: " + name);
3284 }
3285
3286 if (path === "/") path = "";
3287
3288 if (name.length < 1) {
3289 dlgAlert("Name is too short");
3290 return false;
3291 }
3292
3293 let int_array_length = parseInt(arraylength);
3294
3295 //alert("int_array_length: " + int_array_length);
3296
3297 if (!int_array_length || int_array_length < 1) {
3298 dlgAlert("Bad array length: " + arraylength);
3299 return false;
3300 }
3301
3302 let int_string_length = parseInt(stringlength);
3303
3304 if (!int_string_length || int_string_length < 1) {
3305 dlgAlert("Bad string length " + stringlength);
3306 return false;
3307 }
3308
3309 let param = {};
3310 param.path = path + "/" + name;
3311 param.type = parseInt(type);
3312 if (int_array_length > 1)
3313 param.array_length = int_array_length;
3314 if (int_string_length > 0)
3315 param.string_length = int_string_length;
3316
3317 mjsonrpc_db_create([param]).then(function (rpc) {
3318 let status = rpc.result.status[0];
3319 if (status === 311) {
3320 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3321 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3322 });
3323 } else if (status !== 1) {
3324 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages.", true, true, function () {
3325 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3326 });
3327 } else {
3328 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3329 }
3330 }).catch(function (error) {
3331 mjsonrpc_error_alert(error);
3332 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3333 });
3334
3335 return false;
3336}
3337
3338function mhttpd_create_page_handle_cancel(mouseEvent) {
3339 dlgHide('dlgCreate');
3340 return false;
3341}
3342
3343function mhttpd_link_page_handle_link(mouseEvent) {
3344 let e = document.getElementById("link_odbpath");
3345 let path = JSON.parse(e.innerHTML);
3346 if (path === "/") path = "";
3347 //let path = document.getElementById("odb_path").value;
3348 let name = document.getElementById("link_name").value;
3349 let target = document.getElementById("link_target").value;
3350
3351 //console.log("Path: " + path + " Name: " + name + " Target: [" + target + "]");
3352
3353 if (name.length < 1) {
3354 dlgAlert("Name is too short");
3355 return false;
3356 }
3357
3358 if (target.length <= 1) {
3359 dlgAlert("Link target is too short");
3360 return false;
3361 }
3362
3363 let param = {};
3364 param.new_links = [path + "/" + name];
3365 param.target_paths = [target];
3366
3367 mjsonrpc_call("db_link", param).then(function (rpc) {
3368 let status = rpc.result.status[0];
3369 if (status === 304) {
3370 dlgMessage("Error", "Invalid link, see MIDAS messages.", true, true, function () {
3371 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3372 });
3373 } else if (status === 311) {
3374 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3375 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3376 });
3377 } else if (status === 312) {
3378 dlgMessage("Error", "Target path " + target + " does not exist in ODB.", true, true, function () {
3379 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3380 });
3381 } else if (status === 315) {
3382 dlgMessage("Error", "ODB data type mismatch, see MIDAS messages.", true, true, function () {
3383 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3384 });
3385 } else if (status !== 1) {
3386 dlgMessage("Error", "db_create_link() error " + status + ", see MIDAS messages.", true, true, function () {
3387 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3388 });
3389 } else {
3390 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3391 }
3392 }).catch(function (error) {
3393 mjsonrpc_error_alert(error);
3394 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3395 });
3396
3397 return false;
3398}
3399
3400function mhttpd_link_page_handle_cancel(mouseEvent) {
3401 dlgHide('dlgLink');
3402 return false;
3403}
3404
3405function mhttpd_delete_page_handle_delete(mouseEvent, xpath) {
3406 let form = document.getElementsByTagName('form')[0];
3407 let path;
3408 let names = [];
3409
3410 if (form) {
3411 path = form.elements['odb'].value;
3412
3413 if (path === "/") path = "";
3414
3415 for (let i = 0; ; i++) {
3416 let n = "name" + i;
3417 let v = form.elements[n];
3418 if (v === undefined)
3419 break;
3420 if (v.checked)
3421 names.push(path + "/" + v.value);
3422 }
3423 } else {
3424 let e = document.getElementById("odbpath");
3425 path = JSON.parse(e.innerHTML);
3426 if (path === "/") path = "";
3427
3428 //alert("Path: " + path);
3429
3430 for (i = 0; ; i++) {
3431 let v = document.getElementById("delete" + i);
3432 if (v === undefined || v === null)
3433 break;
3434 if (v.checked) {
3435 let name = JSON.parse(v.value);
3436 if (name.length > 0) {
3437 names.push(path + "/" + name);
3438 }
3439 }
3440 }
3441
3442 //alert("Names: " + names);
3443 //return false;
3444 }
3445
3446 if (names.length < 1) {
3447 dlgAlert("Please select at least one ODB entry to delete.");
3448 return false;
3449 }
3450
3451 //alert(names);
3452
3453 let params = {};
3454 params.paths = names;
3455 mjsonrpc_call("db_delete", params).then(function (rpc) {
3456 let message = "";
3457 let status = rpc.result.status;
3458 //alert(JSON.stringify(status));
3459 for (let i = 0; i < status.length; i++) {
3460 if (status[i] !== 1) {
3461 message += "Cannot delete \"" + rpc.request.params.paths[i] + "\", db_delete_key() status " + status[i] + "\n";
3462 }
3463 }
3464 if (message.length > 0) {
3465 dlgAlert(message);
3466 } else {
3467 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3468 }
3469 }).catch(function (error) {
3470 mjsonrpc_error_alert(error);
3471 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3472 });
3473
3474 return false;
3475}
3476
3477function mhttpd_delete_page_handle_cancel(mouseEvent) {
3478 dlgHide('dlgDelete');
3479 return false;
3480}
3481
3482function mhttpd_start_run(ret) {
3483 mhttpd_goto_page("Start", ret); // DOES NOT RETURN
3484}
3485
3486function mhttpd_stop_run(ret) {
3487 dlgConfirm('Are you sure to stop the run?', function (flag) {
3488 if (flag === true) {
3489 mjsonrpc_call("cm_transition", {"transition": "TR_STOP"}).then(function (rpc) {
3490 //mjsonrpc_debug_alert(rpc);
3491 if (rpc.result.status !== 1) {
3492 throw new Error("Cannot stop run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3493 }
3494 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3495 }).catch(function (error) {
3496 mjsonrpc_error_alert(error);
3497 });
3498 }
3499
3500 });
3501}
3502
3503function mhttpd_pause_run(ret) {
3504 dlgConfirm('Are you sure to pause the run?', function (flag) {
3505 if (flag === true) {
3506 mjsonrpc_call("cm_transition", {"transition": "TR_PAUSE"}).then(function (rpc) {
3507 //mjsonrpc_debug_alert(rpc);
3508 if (rpc.result.status !== 1) {
3509 throw new Error("Cannot pause run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3510 }
3511 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3512 }).catch(function (error) {
3513 mjsonrpc_error_alert(error);
3514 });
3515 }
3516 });
3517}
3518
3519
3520function mhttpd_resume_run(ret) {
3521 dlgConfirm('Are you sure to resume the run?', function (flag) {
3522 if (flag === true) {
3523 mjsonrpc_call("cm_transition", {"transition": "TR_RESUME"}).then(function (rpc) {
3524 //mjsonrpc_debug_alert(rpc);
3525 if (rpc.result.status !== 1) {
3526 throw new Error("Cannot resume run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3527 }
3528 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3529 }).catch(function (error) {
3530 mjsonrpc_error_alert(error);
3531 });
3532 }
3533 });
3534}
3535
3536function mhttpd_cancel_transition() {
3537 dlgConfirm('Are you sure to cancel the currently active run transition?', function (flag) {
3538 if (flag === true) {
3539 let paths = [];
3540 let values = [];
3541
3542 paths.push("/Runinfo/Requested Transition");
3543 values.push(0);
3544 paths.push("/Runinfo/Transition in progress");
3545 values.push(0);
3546
3547 let params = {};
3548 params.paths = paths;
3549 params.values = values;
3550
3551 mjsonrpc_call("db_paste", params).then(function (rpc) {
3552 //mjsonrpc_debug_alert(rpc);
3553 if ((rpc.result.status[0] !== 1) || (rpc.result.status[1] !== 1)) {
3554 throw new Error("Cannot cancel transition, db_paste() status " + rpc.result.status + ", see MIDAS messages");
3555 }
3556 mhttpd_goto_page("Transition"); // DOES NOT RETURN
3557 }).catch(function (error) {
3558 mjsonrpc_error_alert(error);
3559 });
3560 }
3561 });
3562}
3563
3564function mhttpd_reset_alarm(alarm_name) {
3565 mhttpd_alarm_snooze_clear();
3566 mjsonrpc_call("al_reset_alarm", {"alarms": [alarm_name]}).then(function (rpc) {
3567 //mjsonrpc_debug_alert(rpc);
3568 if (rpc.result.status[0] !== 1 && rpc.result.status[0] !== 1004) {
3569 throw new Error("Cannot reset alarm, status " + rpc.result.status + ", see MIDAS messages");
3570 }
3571 }).catch(function (error) {
3572 mjsonrpc_error_alert(error);
3573 });
3574}
3575
3576/*---- site and session storage ----------------------------*/
3577
3578/*
3579 Usage:
3580
3581 flag = mhttpdConfig().speakChat; // read
3582
3583 mhttpdConfigSet('speakChat', false); // write individual config
3584
3585 let c = mhttpdConfig(); // write whole config
3586 c.speakChat = false;
3587 c.... = ...;
3588 mhttpdConfigSetAll(c);
3589
3590
3591 Saves settings are kept in local storage, which gets
3592 cleared when the browser session ends. Then the default
3593 values are returned.
3594 */
3595
3596let mhttpd_config_defaults = {
3597 'chatName': "",
3598
3599 'pageTalk': true,
3600 'pageError': true,
3601 'pageInfo': true,
3602 'pageLog': false,
3603
3604 'displayChat': true,
3605 'displayTalk': true,
3606 'displayError': true,
3607 'displayInfo': false,
3608 'displayLog': false,
3609
3610 'speakChat': true,
3611 'speakTalk': true,
3612 'speakError': false,
3613 'speakInfo': false,
3614 'speakLog': false,
3615
3616 'displayChat': true,
3617 'displayTalk': true,
3618 'displayError': true,
3619 'displayInfo': true,
3620 'displayLog': false,
3621
3622 'speakVoice': 'Alex',
3623 'speakVolume': 1,
3624
3625 'alarmSound': true,
3626 'alarmSoundFile': 'beep.mp3',
3627 'alarmRepeat': 60,
3628 'alarmVolume': 1,
3629
3630 'timezone': 'local',
3631
3632 'var': {
3633 'lastSpeak': 0,
3634 'lastAlarm': 0
3635 },
3636
3637 'suppressMessageBefore': 0,
3638 'showMenu': true,
3639
3640 'facility': 'midas',
3641
3642 'fontSize': '10'
3643};
3644
3645function mhttpdConfigODB(callback) {
3646 let c = mhttpd_config_defaults;
3647 try {
3648 if (localStorage.mhttpd) {
3649 c = JSON.parse(localStorage.mhttpd);
3650 callback();
3651 } else {
3652 // obtain some defaults from ODB
3653 mjsonrpc_db_get_value("/Experiment/Enable sound").then(function (rpc) {
3654 let flag = rpc.result.data[0];
3655 if (flag === null) {
3656 mjsonrpc_db_create([{"path" : "/Experiment/Enable sound", "type" : TID_BOOL}]).then(function (rpc) {
3657 mjsonrpc_db_paste(["/Experiment/Enable sound"],[true]).then(function(rpc) {}).catch(function(error) {
3658 mjsonrpc_error_alert(error); });
3659 }).catch(function (error) {
3660 mjsonrpc_error_alert(error);
3661 });
3662 } else if (flag === false) {
3663 c.speakChat = false;
3664 c.speakTalk = false;
3665 c.speakError = false;
3666 c.speakInfo = false;
3667 c.speakLog = false;
3668 c.alarmSound = false;
3669 localStorage.setItem('mhttpd', JSON.stringify(c));
3670 }
3671 callback();
3672 }).catch(function (error) {
3673 mjsonrpc_error_alert(error);
3674 });
3675 }
3676 } catch (e) {
3677 }
3678}
3679
3680function mhttpdConfig() {
3681 let c = mhttpd_config_defaults;
3682 try {
3683 if (localStorage.mhttpd)
3684 c = JSON.parse(localStorage.mhttpd);
3685
3686 // if element has been added to mhttpd_config_defaults, merge it
3687 if (Object.keys(c).length !== Object.keys(mhttpd_config_defaults).length) {
3688 for (let o in mhttpd_config_defaults)
3689 if (!(o in c))
3690 c[o] = mhttpd_config_defaults[o];
3691 }
3692 } catch (e) {
3693 }
3694
3695 return c;
3696}
3697
3698function mhttpdConfigSet(item, value) {
3699 try {
3700 let c = mhttpdConfig();
3701 if (item.indexOf('.') > 0) {
3702 let c1 = item.substring(0, item.indexOf('.'));
3703 let c2 = item.substring(item.indexOf('.') + 1);
3704 c[c1][c2] = value;
3705 } else
3706 c[item] = value;
3707 localStorage.setItem('mhttpd', JSON.stringify(c));
3708 } catch (e) {
3709 }
3710}
3711
3712function mhttpdConfigSetAll(new_config) {
3713 try {
3714 localStorage.setItem('mhttpd', JSON.stringify(new_config));
3715 } catch (e) {
3716 }
3717}
3718
3719/*---- sound and speak functions --------------------------*/
3720
3721let last_audio = null;
3722let inside_new_audio = false;
3723let count_audio = 0;
3724let mhttpd_alarm_play_pending = false;
3725
3726const mhttpd_alarm_snooze_storage_key = "mhttpdAlarmSnoozeUntil";
3727
3728function mhttpd_alarm_snooze_remaining() {
3729 let snooze_until = 0;
3730
3731 try {
3732 snooze_until = parseInt(sessionStorage.getItem(mhttpd_alarm_snooze_storage_key), 10);
3733 } catch (e) {
3734 return 0;
3735 }
3736
3737 if (!Number.isFinite(snooze_until)) {
3738 sessionStorage.removeItem(mhttpd_alarm_snooze_storage_key);
3739 return 0;
3740 }
3741
3742 let remaining = Math.ceil((snooze_until - Date.now()) / 1000);
3743 if (remaining <= 0) {
3744 sessionStorage.removeItem(mhttpd_alarm_snooze_storage_key);
3745 return 0;
3746 }
3747
3748 return remaining;
3749}
3750
3751function mhttpd_alarm_is_snoozed() {
3752 return mhttpd_alarm_snooze_remaining() > 0;
3753}
3754
3755function mhttpd_alarm_snooze(seconds) {
3756 let snooze_until = Date.now() + seconds * 1000;
3757
3758 try {
3759 sessionStorage.setItem(mhttpd_alarm_snooze_storage_key, snooze_until.toString());
3760 } catch (e) {
3761 }
3762 mhttpd_alarm_play_pending = false;
3763
3764 // Stop alarm output that was already in progress when Snooze was selected.
3765 if (last_audio) {
3766 last_audio.pause();
3767 last_audio.src = "";
3768 last_audio = null;
3769 }
3770 if ('speechSynthesis' in window)
3771 speechSynthesis.cancel();
3772
3773 mhttpd_alarm_snooze_update_buttons();
3774}
3775
3776function mhttpd_alarm_snooze_clear() {
3777 try {
3778 sessionStorage.removeItem(mhttpd_alarm_snooze_storage_key);
3779 } catch (e) {
3780 }
3781
3782 // A reset alarm must not suppress the sound of the next alarm through the
3783 // repeat timer left behind by the previous one.
3784 mhttpdConfigSet("var.lastAlarm", 0);
3785 mhttpd_alarm_play_pending = false;
3786
3787 mhttpd_alarm_snooze_close_menus();
3788 mhttpd_alarm_snooze_update_buttons();
3789}
3790
3791function mhttpd_alarm_snooze_close_menus() {
3792 let menus = document.getElementsByClassName("alarmSnoozeMenu");
3793 for (let i = 0; i < menus.length; i++)
3794 menus[i].style.display = "none";
3795}
3796
3797function mhttpd_alarm_snooze_toggle_menu(event) {
3798 if (event.type === "keydown") {
3799 if (event.key !== "Enter" && event.key !== " " || event.repeat)
3800 return;
3801 }
3802
3803 event.preventDefault();
3804 event.stopPropagation();
3805 let menu = event.currentTarget.nextElementSibling;
3806 let show_menu = menu.style.display !== "block";
3807
3808 mhttpd_alarm_snooze_close_menus();
3809 menu.style.display = show_menu ? "block" : "none";
3810}
3811
3812function mhttpd_alarm_snooze_select(event, seconds) {
3813 event.stopPropagation();
3814 mhttpd_alarm_snooze(seconds);
3815 mhttpd_alarm_snooze_close_menus();
3816}
3817
3818function mhttpd_alarm_snooze_update_buttons() {
3819 let remaining = mhttpd_alarm_snooze_remaining();
3820 let label = "Snooze...";
3821
3822 if (remaining > 0) {
3823 let minutes = Math.floor(remaining / 60);
3824 let seconds = remaining % 60;
3825 label = "Snooze " + minutes.toString().padStart(2, "0") + ":" +
3826 seconds.toString().padStart(2, "0");
3827 }
3828
3829 let buttons = document.getElementsByClassName("mAlarmSnoozeButton");
3830 for (let i = 0; i < buttons.length; i++)
3831 buttons[i].textContent = label;
3832}
3833
3834document.addEventListener("click", mhttpd_alarm_snooze_close_menus);
3835
3836//
3837// Do not delete the following code. It is not used at this moment,
3838// but may become necessary in the future. Included
3839// is an alternative method of allocating and releasing Audio objects
3840// and code for tracing Audio object life time and activity. It is needed
3841// to debug interactions between Audio objects and throttled javascript code
3842// in inactive tabs; and with the "user did not interact with this tab" business.
3843// K.O. Jan 2021.
3844//
3845//function mhttpd_alarm_done() {
3846// let ended;
3847// if (last_audio) {
3848// ended = last_audio.ended;
3849// last_audio = null;
3850// }
3851// count_audio_done++;
3852// console.log(Date() + ": mhttpd_alarm_done: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended);
3853//}
3854//
3855//function mhttpd_audio_loadeddata(e) {
3856// console.log(Date() + ": mhttpd_audio_loadeddata: counter " + e.target.counter);
3857//}
3858//
3859//function mhttpd_audio_canplay(e) {
3860// console.log(Date() + ": mhttpd_audio_canplay: counter " + e.target.counter);
3861//}
3862//
3863//function mhttpd_audio_canplaythrough(e) {
3864// console.log(Date() + ": mhttpd_audio_canplaythrough: counter " + e.target.counter);
3865//}
3866//
3867//function mhttpd_audio_ended(e) {
3868// console.log(Date() + ": mhttpd_audio_ended: counter " + e.target.counter);
3869//}
3870//
3871//function mhttpd_audio_paused(e) {
3872// console.log(Date() + ": mhttpd_audio_paused: counter " + e.target.counter);
3873//}
3874
3875function mhttpd_alarm_play_now() {
3876 if (mhttpd_alarm_is_snoozed())
3877 return;
3878
3879 if (last_audio) {
3880 //
3881 // NOTE:
3882 // check for playing the alarm sound is done every few minutes.
3883 // it takes 3 seconds to play the alarm sound
3884 // so in theory, this check is not needed: previous alarm sound should
3885 // always have finished by the time we play a new sound.
3886 //
3887 // However, observed with google-chrome, in inactive and/or iconized tabs
3888 // javascript code is "throttled" and the above time sequence is not necessarily
3889 // observed.
3890 //
3891 // Specifically, I see the following sequence: after 1-2 days of inactivity,
3892 // (i.e.) the "programs" page starts consuming 10-15% CPU, if I open it,
3893 // CPU use goes to 100% for a short while, memory use goes from ~50 Mbytes
3894 // to ~150 Mbytes. console.log() debug print out shows that there are ~400
3895 // accumulated Audio() objects that have been created but did not finish playing
3896 // (because google-chrome is throttling javascript in inactive tabs?), and now
3897 // they all try to load the mp3 file and play all at the same time.
3898 //
3899 // This fix for this observed behaviour is to only create new Audio() objects
3900 // if previous one finished playing.
3901 //
3902 // K.O.
3903 //
3904 if (!last_audio.ended) {
3905 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: previous alarm sound did not finish playing yet");
3906 return;
3907 }
3908 }
3909
3910 if (inside_new_audio) {
3911 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: already inside \"new Audio()\"");
3912 return;
3913 }
3914 inside_new_audio = true;
3915
3916 // reference counting of Audio objects. Not in use today. Do not remove. K.O. Jan 2021
3917 //console.log(Date() + ": mhttpd_alarm_play: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended + ", audio.play!");
3918 //count_audio_created++;
3919
3920 let audio = new Audio(mhttpdConfig().alarmSoundFile);
3921 audio.volume = mhttpdConfig().alarmVolume;
3922 audio.counter = ++count_audio;
3923
3924 last_audio = audio;
3925 inside_new_audio = false;
3926
3927 // code for tracing activity of Audio objects. do not remove. K.O. Jan 2021
3928 //audio.addEventListener("loadeddata", mhttpd_audio_loadeddata);
3929 //audio.addEventListener("canplay", mhttpd_audio_canplay);
3930 //audio.addEventListener("canplaythrough", mhttpd_audio_canplaythrough);
3931 //audio.addEventListener("ended", mhttpd_audio_ended);
3932 //audio.addEventListener("paused", mhttpd_audio_paused);
3933
3934 let promise = audio.play();
3935 if (promise) {
3936 promise.then(function(e) {
3937 mhttpd_alarm_play_pending = false;
3938 //console.log(Date() + ": mhttpd_alarm_play: promise fulfilled, counter " + audio.counter);
3939 }).catch(function(e) {
3940 //count_audio_done++;
3941 //console.log(Date() + ": mhttpd_alarm_play: audio.play() exception: " + e + ", created: " + count_audio_created + ", done: " + count_audio_done);
3942 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: audio.play() exception: " + e + ", counter " + audio.counter);
3943 // NB: must clear the URL of the sound file, otherwise, the sound file is still loaded (but not played)
3944 // the loading of sound files is observed to be delayed in inactive tabs
3945 // resulting in many (100-1000) pending loads getting queued, observed
3946 // to all of them attempt to run (load the sound file) in parallel,
3947 // consuming memory (100-300 Mbytes) and CPU (10-15% CPU per inactive tab).
3948 audio.src = "";
3949 // NB: setting audio to pause() does not seem to do anything.
3950 audio.pause();
3951 last_audio = null;
3952 if (e.name === "NotAllowedError") {
3953 // Automatic media playback can be rejected until the document
3954 // receives a user gesture. Do not let the alarm repeat timer hide
3955 // the failed attempt, and retry from the next ordinary click.
3956 mhttpd_alarm_play_pending = true;
3957 mhttpdConfigSet("var.lastAlarm", 0);
3958 }
3959 });
3960 }
3961 //audio.pause();
3962}
3963
3964function mhttpd_alarm_play() {
3965 if (mhttpd_alarm_is_snoozed())
3966 return;
3967
3968 if (mhttpd_alarm_play_pending)
3969 return;
3970
3971 if (mhttpdConfig().alarmSound && mhttpdConfig().alarmSoundFile) {
3972 let now = new Date() / 1000;
3973 let last = mhttpdConfig().var.lastAlarm;
3974 let next = last + parseFloat(mhttpdConfig().alarmRepeat);
3975 let wait = next - now;
3976 let do_play = (now > next);
3977 //console.log("mhttpd_alarm_play: now: " + now + ", next: " + next + ", last: " + last + ", wait: " + wait + ", play: " + do_play);
3978 if (do_play) {
3979 mhttpdConfigSet("var.lastAlarm", now);
3980 mhttpd_alarm_play_now();
3981 }
3982 }
3983}
3984
3985function mhttpd_alarm_retry_pending() {
3986 if (!mhttpd_alarm_play_pending || mhttpd_alarm_is_snoozed())
3987 return;
3988
3989 mhttpd_alarm_play_pending = false;
3990 mhttpd_alarm_play();
3991}
3992
3993document.addEventListener("click", mhttpd_alarm_retry_pending);
3994
3995function mhttpd_speak_now(text) {
3996 if (mhttpd_alarm_is_snoozed())
3997 return;
3998
3999 let u = new SpeechSynthesisUtterance(text);
4000 u.voice = speechSynthesis.getVoices().filter(function (voice) {
4001 return voice.name === mhttpdConfig().speakVoice;
4002 })[0];
4003 u.volume = mhttpdConfig().speakVolume;
4004 speechSynthesis.speak(u);
4005}
4006
4007function mhttpd_speak(time, text) {
4008
4009 if (!('speechSynthesis' in window))
4010 return;
4011
4012 if (mhttpd_alarm_is_snoozed())
4013 return;
4014
4015 if (mhttpdConfig().speakChat) {
4016 if (time > mhttpdConfig().var.lastSpeak) {
4017 mhttpdConfigSet("var.lastSpeak", time);
4018 mhttpd_speak_now(text);
4019 }
4020 }
4021}
4022
4023/* emacs
4024 * Local Variables:
4025 * tab-width: 8
4026 * c-basic-offset: 3
4027 * js-indent-level: 3
4028 * indent-tabs-mode: nil
4029 * End:
4030 */