1/********************************************************************\
4 Created by: Stefan Ritt
6 Contents: JavaScript midas library used by mhttpd
8 Note: please load midas.js before loading mhttpd.js
10 \********************************************************************/
12let run_state_names = {1: "Stopped", 2: "Paused", 3: "Running"};
14let serverTimezoneOffset = undefined;
16let transition_names = {
25// extend 2d canvas object
26CanvasRenderingContext2D.prototype.drawLine = function (x1, y1, x2, y2) {
34// convert json dom values to text for display and editing
35// this is similar to db_sprintf()
37function mie_to_string(tid, jvalue, format) {
38 if (tid === TID_BOOL) {
40 if (format && format[0] === "[") {
43 let cols = format.replace(/[\[\]]/g, "").split(",");
49 return "<span style=\"display: inline-block;" +
50 "width: 1em;height: 1em;" +
51 "border: 1px solid black;" +
52 "background-color: " + col + ";" +
62 if (Array.isArray(jvalue)) {
64 for (let jv of jvalue) {
65 s += mie_to_string(tid, jv, format);
72 if (tid === TID_FLOAT || tid === TID_DOUBLE) {
73 if (jvalue === "NaN" || jvalue === "Infinity" || jvalue === "-Infinity")
76 if (format && format.indexOf("%") !== -1) {
78 let i = format.indexOf('%');
79 let s1 = format.substring(0, format.indexOf('%'));
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] === "%")
94 while (format[i] >= '0' && format[i] <= '9')
96 s3 = format.substring(i);
102 if (format && format.indexOf("e") !== -1) {
103 let p = parseInt(format.substring(format.indexOf("e") + 1));
104 return Number(jvalue).toExponential(p).toString();
106 if (format && format.indexOf("p") !== -1) {
107 let p = parseInt(format.substring(format.indexOf("p") + 1));
108 return Number(jvalue).toPrecision(p).toString();
110 if (format && format.indexOf("f") !== -1) {
111 let p = parseInt(format.substring(format.indexOf("f") + 1));
112 return Number(jvalue).toFixed(p).toString();
114 if (format && format.indexOf("t") !== -1) {
115 return jvalue.toLocaleString();
117 return jvalue.toString();
121 let t = typeof jvalue;
123 if (t === 'number') {
124 jvalue = "" + jvalue;
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) {
131 if (jvalue.substring(0, 2) === '0x')
132 jvalue = "" + parseInt(jvalue, 16);
134 if (format && format.indexOf("%") !== -1) {
136 if (format.length === 1)
139 for (let i = 0; i < format.length; i++) {
140 if (format[i] === '%') {
143 if (format[i] === 'd')
145 if (format[i] === 'e') {
146 let p = parseInt(format.substring(i+1));
147 str += Number(jvalue).toExponential(p).toString();
149 if (format[i] === 'p') {
150 let p = parseInt(format.substring(i+1));
151 str += Number(jvalue).toPrecision(p).toString();
153 if (format[i] === 'f') {
154 let p = parseInt(format.substring(i+1));
155 str += Number(jvalue).toFixed(p).toString();
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] === '%')
167 while (format[i] >= '0' && format[i] <= '9')
180 if (format === undefined)
183 for (let i = 0; i < format.length; i++) {
184 if (format[i] === "d") {
186 str += " / " + jvalue;
190 if (format[i] === "x") {
191 let hex = parseInt(jvalue).toString(16).toUpperCase();
193 str += " / 0x" + hex;
197 if (format[i] === "b") {
198 let bin = parseInt(jvalue).toString(2);
200 str += " / " + bin + "b";
204 if (format[i] === "t") {
205 let loc = parseInt(jvalue).toLocaleString();
217 if (t === 'string') {
221 return jvalue + " (" + t + ")";
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.
230function mhttpd_escape(s) {
233 if (typeof s !== 'string')
236 while (ss.indexOf('"') >= 0)
237 ss = ss.replace('"', '"');
239 while (ss.indexOf('>') >= 0)
240 ss = ss.replace('>', '>');
242 while (ss.indexOf('<') >= 0)
243 ss = ss.replace('<', '<');
245 //console.log("mhttpd_escape: [" + s + "] becomes [" + ss + "]");
250// odb inline edit - make element a link to inline editor
252function mie_back_to_link(p, path) {
253 let link = document.createElement('a');
254 link.href = path + "?cmd=Set";
255 link.innerHTML = "(loading...)";
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);
263 link.innerHTML = mhttpd_escape(mvalue);
264 link.onclick = function () {
265 ODBInlineEdit(p, path);
268 link.onfocus = function () {
269 ODBInlineEdit(p, path);
271 link.style.color = p.oldStyle.color;
273 if (p.childNodes[1] !== undefined &&
274 p.childNodes[1].value !== undefined) //two values means it was editing an array
275 setTimeout(function () {
277 p.removeChild(p.childNodes[1])
280 setTimeout(function () {
282 p.removeChild(p.childNodes[0])
286 if (p.value !== value) {
289 if (p.onchange !== null) {
293 }).catch(function (error) {
294 mjsonrpc_error_alert(error);
299// called from ODBFinishInlineEdit if element has 'confirm' flag
301function ODBConfirmInlineEdit(flag, p) {
304 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
305 value = p.childNodes[1].value;
307 value = p.childNodes[0].value;
310 mjsonrpc_db_set_value(p.odbPath, value).then(function (rpc) {
311 if (parseInt(p.dataset.input) !== 1) {
313 mie_back_to_link(p, p.odbPath);
315 }).catch(function (error) {
316 mjsonrpc_error_alert(error);
319 if (parseInt(p.dataset.input) !== 1) {
321 mie_back_to_link(p, p.odbPath);
328// odb inline edit - write new value to odb
330function ODBFinishInlineEdit(p, path) {
333 if (p.ODBsent === true)
339 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
340 value = p.childNodes[1].value;
342 value = p.childNodes[0].value;
344 //console.log("mie_write odb [" + path + "] value [" + value + "]");
346 // check for validation
347 if (p.dataset.validate !== undefined) {
348 let flag = eval(p.dataset.validate)(value, p);
350 if (parseInt(p.dataset.input) !== 1) {
353 mie_back_to_link(p, path);
359 // validator might have changed the value
360 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
361 value = p.childNodes[1].value;
363 value = p.childNodes[0].value;
366 if (p.dataset.confirm !== undefined) {
368 dlgConfirm(p.dataset.confirm, ODBConfirmInlineEdit, p);
372 mjsonrpc_db_set_value(path, value).then(function (rpc) {
373 if (parseInt(p.dataset.input) !== 1) {
376 mie_back_to_link(p, path);
378 }).catch(function (error) {
379 mjsonrpc_error_alert(error);
384// odb inline edit - key-press handler
386function ODBInlineEditKeydown(event, p, path) {
387 let keyCode = ('which' in event) ? event.which : event.keyCode;
389 if (keyCode === 27) {
391 if (parseInt(p.dataset.input) !== 1) {
394 mie_back_to_link(p, path);
399 if (keyCode === 13) {
400 ODBFinishInlineEdit(p, path);
408// odb inline edit - convert link to edit field
410function mie_link_to_edit(p, cur_val, size) {
411 let string_val = String(cur_val);
414 if (p.tagName === 'A')
415 p.oldStyle = p.style;
416 else if (p.getElementsByTagName('a').length > 0)
417 p.oldStyle = p.getElementsByTagName('a')[0].style;
419 if (size === undefined)
421 let str = mhttpd_escape(string_val);
424 if (p.innerHTML[0] === '[' && p.innerHTML.indexOf(']') !== 0)
425 index = p.innerHTML.substring(0, p.innerHTML.indexOf(']')+1) + " ";
427 p.innerHTML = index + "<input type='text' size='" + size + "' value='" + str +
428 "' onKeydown='return ODBInlineEditKeydown(event, this.parentNode,"" +
429 p.odbPath + ""," + ");' onBlur='ODBFinishInlineEdit(this.parentNode,"" +
430 p.odbPath + ""," + ");' >";
432 // needed for Firefox
433 if (parseInt(p.dataset.input) !== 1)
434 setTimeout(function () {
436 p.childNodes[0].focus();
437 p.childNodes[0].select();
439 p.childNodes[1].focus();
440 p.childNodes[1].select();
446// odb inline edit - start editing
448function ODBInlineEdit(p, odb_path) {
452 p.odbPath = odb_path;
454 mjsonrpc_db_get_values([odb_path]).then(function (rpc) {
455 let value = rpc.result.data[0];
456 if (Array.isArray(value))
458 let tid = rpc.result.tid[0];
459 let format = p.dataset.format;
460 let size = p.dataset.size;
461 if (size === undefined) {
463 if (size === undefined || size < 20)
467 if (format.length > 1) {
468 if (format[0] === 'd' || format[0] === 'x' || format[0] === 'b') {
469 //when going to edit consider only the first format specifier for integers
470 format = String(format[0]);
474 // JSON parser can't cope with locale-based strings (thousands separators), so
475 // force the user to edit the value as a regular number.
479 let mvalue = mie_to_string(tid, value, format);
480 mie_link_to_edit(p, mvalue, size);
481 }).catch(function (error) {
482 mjsonrpc_error_alert(error);
486function dlgOdbEditKeydown(event, input) {
487 let keyCode = ('which' in event) ? event.which : event.keyCode;
489 if (keyCode === 27) {
491 dlgMessageDestroy(input.parentElement.parentElement);
495 if (keyCode === 13) {
496 dlgOdbEditSend(input.parentElement);
497 dlgMessageDestroy(input.parentElement.parentElement);
504function dlgOdbEditSend(b) {
505 let path = b.parentElement.parentElement.parentElement.odbPath;
506 let value = b.parentElement.parentElement.elements[0].value;
508 mjsonrpc_db_set_value(path, value).then(function (rpc) {
509 //mjsonrpc_debug_alert(rpc);
510 }).catch(function (error) {
511 mjsonrpc_error_alert(error);
516// odb edit single value dialog box
518function dlgOdbEdit(path) {
520 mjsonrpc_db_get_value(path).then(function (rpc) {
521 let value = rpc.result.data[0];
522 let tid = rpc.result.tid[0];
523 value = mie_to_string(tid, value);
525 d = document.createElement("div");
526 d.className = "dlgFrame";
530 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">Change ODB value</div>" +
532 "<div class=\"dlgPanel\" style=\"padding: 30px;\">" +
533 "<div id=\"dlgMessageString\">" +
534 path + " : " +
535 "<input type='text' size='16' value='" + value + "' onkeydown='return dlgOdbEditKeydown(event, this);'>" +
538 "<button class=\"dlgButton\" id=\"dlgMessageButton\" style=\"background-color:#F8F8F8\" type=\"button\" " +
539 " onClick=\"dlgOdbEditSend(this);dlgMessageDestroy(this.parentElement);\">Ok</button>" +
540 "<button class=\"dlgButton\" id=\"dlgMessageButton\" style=\"background-color:#F8F8F8\" type=\"button\" " +
541 " onClick=\"dlgMessageDestroy(this.parentElement)\">Cancel</button>" +
545 document.body.appendChild(d);
549 // needed for Firefox
550 setTimeout(function () {
551 d.childNodes[1].elements[0].focus();
552 d.childNodes[1].elements[0].select();
556 }).catch(function (error) {
557 mjsonrpc_error_alert(error);
561/*---- mhttpd functions -------------------------------------*/
564// Returns time to display in status bar and history.
565// By default show local time, but through the config page the
566// user can select the server time zone or any other time zone
568function mhttpd_get_display_time(sec) {
569 // retrieve timezone of server on the first call
570 if (serverTimezoneOffset === undefined) {
571 mjsonrpc_get_timezone().then(function (rpc) {
572 serverTimezoneOffset = rpc.result;
573 }).catch(function (error) {
574 mjsonrpc_error_alert(error);
579 if (sec !== undefined)
580 d = new Date(sec * 1000);
585 if (mhttpdConfig().timezone === undefined)
586 mhttpdConfigSet('timezone', 'local');
588 if (mhttpdConfig().timezone === 'local') {
589 o = -d.getTimezoneOffset() / 60;
590 } else if (mhttpdConfig().timezone === 'server') {
591 if (serverTimezoneOffset === undefined)
597 o = serverTimezoneOffset / 3600;
599 o = parseInt(mhttpdConfig().timezone);
602 d = new Date(d.getTime() + o * 3600 * 1000);
604 let s = d.toLocaleString("en-gb", {
606 hour12: false, day: 'numeric', month: 'short', year: 'numeric',
607 hour: 'numeric', minute: 'numeric', second: 'numeric'
624function mhttpd_disable_button(button) {
625 if (!button.disabled) {
626 button.disabled = true;
630function mhttpd_enable_button(button) {
631 if (button.disabled) {
632 button.disabled = false;
636function mhttpd_set_style_visibility(e, v) {
638 if (e.style.visibility !== v) {
639 e.style.visibility = v;
644function mhttpd_set_style_display(e, v) {
646 if (e.style.display !== v) {
652function mhttpd_set_firstChild_data(e, v) {
654 if (e.firstChild.data !== v) {
655 //console.log("mhttpd_set_firstChild_data for " + e + " from " + e.firstChild.data + " to " + v);
656 e.firstChild.data = v;
661function mhttpd_set_innerHTML(e, v) {
663 if (e.innerHTML !== v) {
664 //console.log("mhttpd_set_innerHTML for " + e + " from " + e.innerHTML + " to " + v);
670function mhttpd_set_className(e, v) {
672 if (e.className !== v) {
673 //console.log("mhttpd_set_className for " + e + " from " + e.className + " to " + v);
679function mhttpd_hide_button(button) {
680 mhttpd_set_style_visibility(button, "hidden");
681 mhttpd_set_style_display(button, "none");
684function mhttpd_unhide_button(button) {
685 mhttpd_set_style_visibility(button, "visible");
686 mhttpd_set_style_display(button, "");
689function mhttpd_hide(id) {
690 let e = document.getElementById(id);
692 mhttpd_set_style_visibility(e, "hidden");
693 mhttpd_set_style_display(e, "none");
697function mhttpd_unhide(id) {
698 let e = document.getElementById(id);
700 mhttpd_set_style_visibility(e, "visible");
701 mhttpd_set_style_display(e, "");
705function mhttpd_enable(id) {
706 let e = document.getElementById(id);
711function mhttpd_disable(id) {
712 let e = document.getElementById(id);
717function mhttpd_init_overlay(overlay) {
718 mhttpd_hide_overlay(overlay);
720 // this element will hide the underlaying web page
722 overlay.style.zIndex = 10;
723 //overlay.style.backgroundColor = "rgba(0,0,0,0.5)"; /*dim the background*/
724 overlay.style.backgroundColor = "white";
725 overlay.style.position = "fixed";
726 overlay.style.top = "0%";
727 overlay.style.left = "0%";
728 overlay.style.width = "100%";
729 overlay.style.height = "100%";
731 return overlay.children[0];
734function mhttpd_hide_overlay(overlay) {
735 overlay.style.visibility = "hidden";
736 overlay.style.display = "none";
739function mhttpd_unhide_overlay(overlay) {
740 overlay.style.visibility = "visible";
741 overlay.style.display = "";
744function mhttpd_getParameterByName(name) {
745 let match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
746 return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
749function mhttpd_goto_page(page, param) {
750 if (!param) param = "";
751 window.location.href = '?cmd=' + page + param; // reloads the page from new URL
755function mhttpd_navigation_bar(current_page, path) {
756 document.write("<div id='customHeader'>\n");
757 document.write("</div>\n");
759 document.write("<div class='mnav'>\n");
760 document.write(" <table>\n");
761 document.write(" <tr><td id='navigationTableButtons'></td></tr>\n");
762 document.write(" </table>\n\n");
763 document.write("</div>\n");
768 if (localStorage.mNavigationButtons !== undefined) {
769 document.getElementById("navigationTableButtons").innerHTML = localStorage.mNavigationButtons;
770 let button = document.getElementById("navigationTableButtons").children;
771 for (let i = 0; i < button.length; i++)
772 if (button[i].value.toLowerCase() === current_page.toLowerCase())
773 button[i].className = "mnav mnavsel navButtonSel";
775 button[i].className = "mnav navButton";
779 mjsonrpc_db_get_values(["/Custom/Header", "/Experiment/Menu", "/Experiment/Menu Buttons"]).then(function (rpc) {
780 let custom_header = rpc.result.data[0];
782 if (custom_header && custom_header.length > 0)
783 document.getElementById("customHeader").innerHTML = custom_header;
785 let menu = rpc.result.data[1];
786 let buttons = rpc.result.data[2];
790 for (let k in menu) {
791 let kk = k + "/name";
798 } else if (buttons && buttons.length > 0) {
799 b = buttons.split(",");
802 if (!b || b.length < 1) {
803 b = ["Status", "ODB", "Messages", "Chat", "ELog", "Alarms", "Programs", "History", "MSCB", "Sequencer", "Config", "Help"];
808 for (let i = 0; i < b.length; i++) {
809 let bb = b[i].trim();
810 let cc = "mnav navButton";
811 if (bb.toLowerCase() === current_page.toLowerCase()) {
812 cc = "mnav mnavsel navButtonSel";
814 html += "<input type=button name=cmd value='" + bb + "' class='" + cc + "' onclick='window.location.href=\'" + path + "?cmd=" + bb + "\';return false;'>\n";
816 document.getElementById("navigationTableButtons").innerHTML = html;
818 // cache navigation buttons in browser local storage
819 localStorage.setItem("mNavigationButtons", html);
821 }).catch(function (error) {
822 mjsonrpc_error_alert(error);
826function mhttpd_show_menu(flag) {
827 let m = document.getElementById("msidenav");
829 if (m.initialWidth === undefined)
830 m.initialWidth = m.clientWidth;
833 m.style.width = m.initialWidth + "px";
834 document.getElementById("mmain").style.marginLeft = m.initialWidth + "px";
835 mhttpdConfigSet('hideMenu', false);
838 document.getElementById("mmain").style.marginLeft = "0";
841 mhttpdConfigSet('showMenu', flag);
844function mhttpd_toggle_menu() {
845 let flag = mhttpdConfig().showMenu;
847 mhttpd_show_menu(flag);
850function mhttpd_exec_script(name) {
851 //console.log("exec_script: " + name);
852 let params = new Object;
853 params.script = name;
854 mjsonrpc_call("exec_script", params).then(function (rpc) {
855 let status = rpc.result.status;
857 dlgAlert("Exec script \"" + name + "\" status " + status);
859 }).catch(function (error) {
860 mjsonrpc_error_alert(error);
864let mhttpd_refresh_id;
865let mhttpd_refresh_history_id;
866let mhttpd_refresh_interval;
867let mhttpd_refresh_paused;
868let mhttpd_refresh_history_interval;
869let mhttpd_spinning_wheel;
870let mhttpd_initialized = false;
872function mhttpd_init(current_page, interval, callback) {
874 This function should be called from custom pages to initialize all ODB tags and refresh
875 them periodically every "interval" in ms
877 For possible ODB tags please refer to
879 https://midas.triumf.ca/MidasWiki/index.php/New_Custom_Pages_(2017)
882 if (mhttpd_initialized) {
883 dlgAlert("mhttpd_init() is called more than once from user code");
888 url = mhttpd_getParameterByName("URL");
890 mjsonrpc_set_url(url);
892 // retrieve current page if not given
893 if (current_page === undefined) {
894 current_page = mhttpd_getParameterByName("page");
896 if (current_page === undefined || current_page === null) {
897 dlgAlert("Please specify name of page via mhttpd_init('<name>')<br>or make sure to have '&page=<name>' in URL");
902 let h = document.getElementById("mheader");
904 dlgAlert('Web page does not contain "mheader" element');
907 let s = document.getElementById("msidenav");
909 dlgAlert('Web page does not contain "msidenav" element');
913 mhttpd_initialized = true;
915 // set global font size
916 let config = mhttpdConfig();
917 document.body.style.fontSize = config.fontSize + "pt";
919 h.style.display = "flex";
921 "<div style='display:inline-block; flex:none;'>" +
922 "<span class='mmenuitem' style='padding: 10px;margin-right: 20px;' onclick='mhttpd_toggle_menu()'>☰</span>" +
923 "<span id='mheader_expt_name'></span>" +
926 "<div style='flex:auto;'>" +
927 " <div id='mheader_message'></div>" +
930 "<div style='display: inline; flex:none;'>" +
931 " <div id='mheader_alarm'> </div>" +
932 " <div style='display: inline; font-size: 75%; margin-right: 10px' id='mheader_last_updated'>mheader_last_updated</div>" +
935 mhttpd_resize_sidenav();
936 window.addEventListener('resize', mhttpd_resize_sidenav);
938 // put error header in front of header
939 let d = document.createElement('div');
940 d.id = 'mheader_error';
941 h.parentNode.insertBefore(d, h);
943 // update header and menu
944 if (document.getElementById("msidenav") !== undefined) {
946 // get it from session storage cache if present
947 if (sessionStorage.msidenav !== undefined && sessionStorage.mexpname !== undefined) {
948 let menu = document.getElementById("msidenav");
949 menu.innerHTML = sessionStorage.msidenav;
950 let item = menu.children;
951 for (let i = 0; i < item.length; i++) {
952 if (item[i].className !== "mseparator") {
953 if (current_page.search(item[i].innerHTML) !== -1)
954 item[i].className = "mmenuitem mmenuitemsel";
956 item[i].className = "mmenuitem";
959 document.getElementById("mheader_expt_name").innerHTML = sessionStorage.mexpname;
961 // now the side navigation has its full width, adjust the main body and make it visible
962 let m = document.getElementById("mmain");
963 if (m !== undefined) {
964 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
965 m.style.opacity = "1";
969 // request it from server, since it might have changed
970 mjsonrpc_db_get_values(["/Experiment/Name", "/Experiment/Menu", "/Experiment/Menu Buttons",
971 "/Custom", "/Script", "/Alias"]).then(function (rpc) {
973 let expt_name = rpc.result.data[0];
974 let menu = rpc.result.data[1];
975 let buttons = rpc.result.data[2];
976 let custom = rpc.result.data[3];
977 let script = rpc.result.data[4];
978 let alias = rpc.result.data[5];
980 document.getElementById("mheader_expt_name").innerHTML = expt_name;
981 sessionStorage.setItem("mexpname", expt_name);
983 // preload spinning wheel for later use
984 if (mhttpd_spinning_wheel === undefined) {
985 mhttpd_spinning_wheel = new Image();
986 mhttpd_spinning_wheel.src = "spinning-wheel.gif";
992 for (let k in menu) {
993 if (k.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
995 if (menu[k]) // show button if not disabled
996 b.push(menu[k + "/name"]);
998 } else if (buttons && buttons.length > 0) {
999 b = buttons.split(",");
1002 if (!b || b.length < 1) {
1003 b = ["Status", "ODB", "Messages", "Chat", "ELog", "Alarms", "Programs", "History", "MSCB", "Sequencer", "Config", "Help"];
1008 for (let i = 0; i < b.length; i++) {
1009 let bb = b[i].trim();
1010 let cc = "mmenuitem";
1011 if (bb === current_page) {
1012 cc += " mmenuitemsel";
1014 html += "<div class='" + cc + "'><a href='?cmd=" + bb + "' class='mmenulink'>" + bb + "</a></div>\n";
1018 if (custom !== null && Object.keys(custom).length > 0) {
1020 html += "<div class='mseparator'></div>\n";
1021 // add menu items recursively
1022 html = mhttpd_add_menu_items(html, custom, current_page, "", 0);
1026 if (script !== null && Object.keys(script).length > 0) {
1028 html += "<div class='mseparator'></div>\n";
1030 for (let b in script) {
1031 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1033 let n = script[b + "/name"];
1034 //html += "<div class='mmenuitem'><a href='?script=" + b + "' class='mmenulink'>" + n + "</a></div>\n";
1035 html += "<div class='mmenuitem'><button class='mbutton' onclick='mhttpd_exec_script(\"" + n + "\")'>" + n + "</button></div>\n";
1041 if (alias !== null && Object.keys(alias).length > 0) {
1043 html += "<div class='mseparator'></div>\n";
1046 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1048 n = alias[b + "/name"];
1049 if (n.substr(n.length - 1) === "&") {
1050 n = n.substr(0, n.length - 1);
1051 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink' target='_blank'>" + n + "↗</a></div>\n";
1053 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink'>" + n + "</a></div>\n";
1059 // dummy spacer to fix scrolling to the bottom, must be at least header height
1060 html += "<div style='height: 64px;'></div>\n";
1062 document.getElementById("msidenav").innerHTML = html;
1064 // re-adjust size of mmain element if menu has changed
1065 let m = document.getElementById("mmain");
1066 if (m !== undefined) {
1067 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
1068 m.style.opacity = "1";
1071 // cache navigation buttons in browser local storage
1072 sessionStorage.setItem("msidenav", html);
1074 // show/hide sidenav according to local storage settings
1075 mhttpd_show_menu(mhttpdConfig().showMenu);
1077 // hide all invisible menu items after layout has been finished
1078 let mi = document.getElementsByClassName('mmenuitem');
1080 let p = m.parentElement;
1081 if (p.style.visibility === "hidden") {
1082 p.style.visibility = "visible";
1083 p.style.display = "none";
1086 mi = document.getElementsByClassName('msubmenuitem');
1088 let p = m.parentElement;
1089 if (p.style.visibility === "hidden") {
1090 p.style.visibility = "visible";
1091 p.style.display = "none";
1095 }).then(function () {
1096 if (callback !== undefined)
1098 }).catch(function (error) {
1099 mjsonrpc_error_alert(error);
1103 // store refresh interval and do initial refresh
1104 if (interval === undefined)
1106 mhttpd_refresh_interval = interval;
1107 mhttpd_refresh_paused = false;
1109 // history interval is static for now
1110 mhttpd_refresh_history_interval = 30000;
1112 // trigger first refresh
1114 mhttpd_refresh_history();
1117function mhttpd_find_menuitem(current_page, submenu) {
1118 for (let b in submenu) {
1120 if (typeof submenu[b] === "object") { // recurse into submenu
1121 let flag = mhttpd_find_menuitem(current_page, submenu[b]);
1126 if (typeof submenu[b] === "string") {
1128 if (s[s.length-1] === '&')
1131 if (b.indexOf('/name') !== -1 && s.toLowerCase() === current_page.toLowerCase())
1140function mhttpd_add_menu_items(html, custom, current_page, path, level) {
1141 for (let b in custom) {
1142 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1144 if (b === "path" || b === "images" || b === "default") // skip "path", "Images" and "default"
1147 if (typeof custom[b] === "object") { // submenu created by subdirectory in ODB
1148 let cc = "msubmenuitem";
1150 let expand = mhttpd_find_menuitem(current_page, custom[b]);
1152 // add blanks according to nesting level
1153 for (let i=0 ; i<level ; i++)
1154 l += " ";
1157 l += "▾ "; // caret down
1159 l += "▸ "; // caret right
1162 l += custom[b + "/name"];
1168 html += "<div class='" + cc + "' onclick='mhttpd_submenu(this)'><div class='mmenulink'>" + l + "</div></div>\n";
1171 html += "<div>"; // do not hide submenu if current page is under it
1173 html += "<div style='visibility: hidden'>";
1174 html = mhttpd_add_menu_items(html, custom[b], current_page, p, level+1);
1176 } else if (typeof custom[b] === "string") { // skip any items that don't have type of string, since can't be valid links
1177 let cc = "mmenuitem";
1178 let mitem = custom[b + "/name"];
1179 if (mitem.slice(-1) === '&')
1180 mitem = mitem.slice(0, -1);
1181 if (current_page.search(mitem) !== -1 ||
1182 current_page.search(mitem.toLowerCase()) !== -1)
1183 cc += " mmenuitemsel";
1185 for (let i = 0; i < level; i++)
1186 ln += " ";
1187 ln += custom[b + "/name"];
1189 if (custom[b].substring(0, 5) === "link:") {
1190 let lt = custom[b].substring(5);
1191 html += "<div class='" + cc + "'><a href='" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1196 lt += custom[b + "/name"];
1197 if (ln.slice(-1) === '!')
1201 let clicklink = false;
1203 // style = "style='display: none'";
1205 // Accepted file extentions, if not create onclick call from value
1206 const validExtensionsRegex = /\.([a-zA-Z0-9]+)$/; // /\.(txt|jpg|jpeg|png|gif|pdf|csv|html|htm|js|css)$/i;
1208 if (ln.slice(-1) === '&') {
1209 ln = ln.slice(0, -1);
1210 lt = lt.slice(0, -1);
1211 target += "target='_blank' ";
1212 } else if (!validExtensionsRegex.test(custom[b])) {
1217 html += `<div ${style} class="mmenuitem" onclick="${custom[b]}"><a style="width: 100%;display: inline-block;text-decoration: none;color: #404040;" class="mmenulink">${ln}</a></div>\n`;
1219 html += "<div class='" + cc + "' " + style + "><a " + target + " href='?cmd=custom&page=" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1229function mhttpd_submenu(o) {
1231 for (i=0 ; o.firstChild.innerHTML.charCodeAt(i) !== 9656 && o.firstChild.innerHTML.charCodeAt(i) !== 9662 ; i++);
1232 if (o.firstChild.innerHTML.charCodeAt(i) === 9656) {
1234 o.firstChild.innerHTML =
1235 o.firstChild.innerHTML.substring(0, i-1) +
1236 String.fromCharCode(9662) +
1237 o.firstChild.innerHTML.substring(i+1);
1238 o.nextElementSibling.style.display = "inline";
1239 o.nextElementSibling.style.visibility = "visible";
1242 o.firstChild.innerHTML =
1243 o.firstChild.innerHTML.substring(0, i-1) +
1244 String.fromCharCode(9656) +
1245 o.firstChild.innerHTML.substring(i+1);
1246 o.nextElementSibling.style.display = "none";
1250function mhttpd_refresh_pause(flag) {
1251 mhttpd_refresh_paused = flag;
1254function mhttpd_set_refresh_interval(interval) {
1255 mhttpd_refresh_interval = interval;
1258function getMElements(name) {
1259 // collect all <div name=[name] >
1261 e.push(...document.getElementsByName(name));
1263 // collect all <div class=[name] >
1264 e.push(...document.getElementsByClassName(name));
1269function mhttpd_thermo_draw() {
1270 let ctx = this.firstChild.getContext("2d");
1272 let w = this.firstChild.width;
1273 let h = this.firstChild.height;
1274 ctx.clearRect(0, 0, w, h);
1275 w = w - 1; // space for full circles
1278 if (this.dataset.scale === "1") {
1280 w = Math.floor(w / 4) * 4;
1283 if (this.dataset.printValue === "1") {
1287 let x0 = Math.round(w / 4 * 0);
1288 let x1 = Math.round(w / 4 * 1);
1289 let x2 = Math.round(w / 4 * 2);
1290 let x3 = Math.round(w / 4 * 3);
1293 if (v < this.dataset.minValue)
1294 v = this.dataset.minValue;
1295 if (v > this.dataset.maxValue)
1296 v = this.dataset.maxValue;
1297 let yt = (h - 4 * x1) - (h - 5 * x1) * (v - this.dataset.minValue) / (this.dataset.maxValue - this.dataset.minValue);
1299 ctx.translate(0.5, 0.5);
1300 ctx.strokeStyle = "#000000";
1301 ctx.fillStyle = "#FFFFFF";
1306 ctx.arc(x2, x1, x1, Math.PI, 0);
1307 ctx.lineTo(x3, h - x1 * 4);
1308 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1309 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1310 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1313 if (this.dataset.backgroundColor !== undefined) {
1314 ctx.fillStyle = this.dataset.backgroundColor;
1319 if (this.dataset.color === undefined) {
1320 ctx.strokeStyle = "#000000";
1321 ctx.fillStyle = "#000000";
1323 ctx.strokeStyle = this.dataset.color;
1324 ctx.fillStyle = this.dataset.color;
1328 ctx.moveTo(x1 + 3, yt);
1329 ctx.lineTo(x3 - 3, yt);
1330 ctx.lineTo(x3 - 3, h - x2);
1331 ctx.lineTo(x1 + 3, h - x2);
1332 ctx.lineTo(x1 + 3, yt);
1337 ctx.arc(x2, h - x2, x2 - 4, 0, 2 * Math.PI);
1341 // re-draw outer "glass"
1342 ctx.strokeStyle = "#000000";
1343 ctx.fillStyle = "#FFFFFF";
1346 ctx.arc(x2, x1, x1, Math.PI, 0);
1347 ctx.lineTo(x3, h - x1 * 4);
1348 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1349 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1350 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1355 if (this.dataset.scale === "1") {
1357 ctx.moveTo(x3 + x1 / 2, x1);
1358 ctx.lineTo(x3 + x1, x1);
1359 ctx.moveTo(x3 + x1 / 2, h - 4 * x1);
1360 ctx.lineTo(x3 + x1, h - 4 * x1);
1363 ctx.font = "12px sans-serif";
1364 ctx.fillStyle = "#000000";
1365 ctx.strokeStyle = "#000000";
1366 ctx.textBaseline = "middle";
1368 let mintext = this.dataset.minValue;
1369 let maxtext = this.dataset.maxValue;
1371 if (this.dataset.format !== undefined) {
1372 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1373 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1376 ctx.fillText(mintext, 4.5 * x1, h - 4 * x1, 3.5 * x1);
1377 ctx.fillText(maxtext, 4.5 * x1, x1, 3.5 * x1);
1380 // optional value display
1381 if (this.dataset.printValue === "1") {
1382 ctx.font = "12px sans-serif";
1383 ctx.fillStyle = "#000000";
1384 ctx.strokeStyle = "#000000";
1385 ctx.textBaseline = "bottom";
1386 ctx.textAlign = "center";
1388 let valuetext = this.value;
1390 if (this.dataset.format !== undefined) {
1391 valuetext = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1394 ctx.fillText(valuetext, x2, this.firstChild.height, 4 * x1);
1400function mhttpd_gauge_draw() {
1401 let ctx = this.firstChild.getContext("2d");
1403 let w = this.firstChild.width;
1404 let h = this.firstChild.height;
1406 if (this.dataset.scale === "1")
1410 ctx.clearRect(0, 0, w, h);
1413 if (v < this.dataset.minValue)
1414 v = this.dataset.minValue;
1415 if (v > this.dataset.maxValue)
1416 v = this.dataset.maxValue;
1417 v = (v - this.dataset.minValue) / (this.dataset.maxValue - this.dataset.minValue);
1419 ctx.translate(0.5, 0.5);
1420 ctx.strokeStyle = "#000000";
1421 ctx.fillStyle = "#FFFFFF";
1426 ctx.arc(w / 2, y, w / 2 - 1, Math.PI, 0);
1427 ctx.lineTo(w - 4, y);
1428 ctx.arc(w / 2, y, w / 2 - 4, 0, Math.PI, true);
1430 ctx.fillStyle = this.dataset.color;
1435 ctx.fillStyle = this.dataset.color;
1436 ctx.strokeStyle = this.dataset.color;
1437 ctx.arc(w / 2, y, w / 2 - 6, Math.PI, (1 + v) * Math.PI);
1438 ctx.arc(w / 2, y, w / 2 - w / 5, (1 + v) * Math.PI, Math.PI, true);
1442 if (this.dataset.frame) {
1443 ctx.strokeStyle = "#000000";
1446 ctx.arc(w / 2, y, w / 2 - 6, Math.PI, 0);
1447 ctx.lineTo(w - w / 5, y);
1448 ctx.arc(w / 2, y, w / 2 - w / 5, 0, Math.PI, true);
1453 // optional value display
1454 if (this.dataset.printValue === "1") {
1455 ctx.font = "12px sans-serif";
1456 ctx.fillStyle = "#000000";
1457 ctx.strokeStyle = "#000000";
1458 ctx.textBaseline = "bottom";
1459 ctx.textAlign = "center";
1461 let valtext = this.value;
1463 if (this.dataset.format !== undefined) {
1464 valtext = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1467 ctx.fillText(valtext, w / 2, y, w);
1470 // optional scale display
1471 if (this.dataset.scale === "1") {
1472 ctx.font = "12px sans-serif";
1473 ctx.fillStyle = "#000000";
1474 ctx.strokeStyle = "#000000";
1475 ctx.textBaseline = "bottom";
1476 ctx.textAlign = "center";
1478 let mintext = this.dataset.minValue;
1479 let maxtext = this.dataset.maxValue;
1481 if (this.dataset.format !== undefined) {
1482 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1483 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1486 ctx.fillText(mintext, 0.1 * w, h, 0.2 * w);
1487 ctx.fillText(maxtext, 0.9 * w, h, 0.2 * w);
1493function mhttpd_vaxis_draw() {
1494 let ctx = this.firstChild.getContext("2d");
1496 let w = this.firstChild.width;
1497 let h = this.firstChild.height;
1498 ctx.clearRect(0, 0, w, h);
1501 if (this.dataset.line === "0")
1504 if (this.dataset.log === "1")
1509 if (this.dataset.minValue !== undefined)
1510 scaleMin = parseFloat(this.dataset.minValue);
1511 if (log && scaleMin === 0)
1513 if (this.dataset.maxValue !== undefined)
1514 scaleMax = parseFloat(this.dataset.maxValue);
1515 if (scaleMin === scaleMax)
1518 ctx.translate(0.5, 0.5);
1519 ctx.strokeStyle = "#000000";
1520 ctx.fillStyle = "#FFFFFF";
1523 if (this.style.textAlign === "left") {
1524 ctx.translate(-0.5, -0.5);
1525 vaxisDraw(ctx, 0, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1527 ctx.translate(-0.5, -0.5);
1528 vaxisDraw(ctx, w, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1534function mhttpd_haxis_draw() {
1535 let ctx = this.firstChild.getContext("2d");
1537 let w = this.firstChild.width;
1538 let h = this.firstChild.height;
1539 ctx.clearRect(0, 0, w, h);
1542 if (this.dataset.line === "0")
1545 if (this.dataset.log === "1")
1550 if (this.dataset.minValue !== undefined)
1551 scaleMin = parseFloat(this.dataset.minValue);
1552 if (log && scaleMin === 0)
1554 if (this.dataset.maxValue !== undefined)
1555 scaleMax = parseFloat(this.dataset.maxValue);
1556 if (scaleMin === scaleMax)
1559 ctx.translate(-0.5, 0);
1560 ctx.strokeStyle = "#000000";
1561 ctx.fillStyle = "#FFFFFF";
1564 if (this.style.verticalAlign === "top") {
1565 ctx.translate(0.5, 0.5);
1566 haxisDraw(ctx, 1, 0, w - 2, line, 4, 8, 10, 10, 0, scaleMin, scaleMax, log);
1568 ctx.translate(0.5, -0.5);
1569 haxisDraw(ctx, 1, h, w - 2, line, 4, 8, 10, 20, 0, scaleMin, scaleMax, log);
1574String.prototype.stripZeros = function () {
1575 let s = this.trim();
1576 if (s.search("[.]") >= 0) {
1577 let i = s.search("[e]");
1579 while (s.charAt(i - 1) === "0") {
1580 s = s.substring(0, i - 1) + s.substring(i);
1583 if (s.charAt(i - 1) === ".")
1584 s = s.substring(0, i - 1) + s.substring(i);
1586 while (s.charAt(s.length - 1) === "0")
1587 s = s.substring(0, s.length - 1);
1588 if (s.charAt(s.length - 1) === ".")
1589 s = s.substring(0, s.length - 1);
1595function haxisDraw(ctx, x1, y1, width, line, minor, major, text, label, grid, xmin, xmax, logaxis) {
1596 let dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
1597 let tick_base, major_base, label_base, n_sig1, n_sig2, xs;
1598 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1600 ctx.textAlign = "center";
1601 ctx.textBaseline = "top";
1603 if (xmax <= xmin || width <= 0)
1607 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1614 /* use 6 as min tick distance */
1615 dx = (xmax - xmin) / (width / 6);
1617 int_dx = Math.floor(Math.log(dx) / Math.log(10));
1618 frac_dx = Math.log(dx) / Math.log(10) - int_dx;
1625 tick_base = frac_dx < (Math.log(2) / Math.log(10)) ? 1 : frac_dx < (Math.log(5) / Math.log(10)) ? 2 : 3;
1626 major_base = label_base = tick_base + 1;
1628 /* rounding up of dx, label_dx */
1629 dx = Math.pow(10, int_dx) * base[tick_base];
1630 major_dx = Math.pow(10, int_dx) * base[major_base];
1631 label_dx = major_dx;
1634 /* number of significant digits */
1638 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1643 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1645 n_sig1 = Math.max(n_sig1, n_sig2);
1647 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1648 if (Math.abs(xmin) < 100000)
1649 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) + 1);
1650 if (Math.abs(xmax) < 100000)
1651 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) + 1);
1653 /* determination of maximal width of labels */
1654 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
1655 let ext = ctx.measureText(str);
1656 maxwidth = ext.width;
1658 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
1659 ext = ctx.measureText(str);
1660 maxwidth = Math.max(maxwidth, ext.width);
1661 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
1662 maxwidth = Math.max(maxwidth, ext.width);
1664 /* increasing label_dx, if labels would overlap */
1665 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
1667 label_dx = Math.pow(10, int_dx) * base[label_base];
1668 if (label_base % 3 === 2 && major_base % 3 === 1) {
1670 major_dx = Math.pow(10, int_dx) * base[major_base];
1685 x_act = Math.floor(xmin / dx) * dx;
1687 let last_label_x = x1;
1690 ctx.drawLine(x1, y1, x1 + width, y1);
1694 x_screen = (Math.log(x_act) - Math.log(xmin)) / (Math.log(xmax) - Math.log(xmin)) * width + x1;
1696 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
1697 xs = Math.floor(x_screen + 0.5);
1699 if (x_screen > x1 + width + 0.001)
1702 if (x_screen >= x1) {
1703 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
1704 dx / major_dx / 10.0) {
1706 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
1707 dx / label_dx / 10.0) {
1708 /* label tick mark */
1709 ctx.drawLine(xs, y1, xs, y1 + text);
1712 if (grid != 0 && xs > x1 && xs < x1 + width)
1713 ctx.drawLine(xs, y1, xs, y1 + grid);
1717 str = x_act.toPrecision(n_sig1).stripZeros();
1718 ext = ctx.measureText(str);
1720 ctx.fillStyle = "black";
1721 if (xs - ext.width / 2 > x1 &&
1722 xs + ext.width / 2 < x1 + width)
1723 ctx.fillText(str, xs, y1 + label);
1725 last_label_x = xs + ext.width / 2;
1728 /* major tick mark */
1729 ctx.drawLine(xs, y1, xs, y1 + major);
1732 if (grid != 0 && xs > x1 && xs < x1 + width)
1733 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
1742 /* minor tick mark */
1743 ctx.drawLine(xs, y1, xs, y1 + minor);
1745 /* for logaxis, also put labes on minor tick marks */
1748 str = x_act.toPrecision(n_sig1).stripZeros();
1749 ext = ctx.measureText(str);
1751 ctx.fillStyle = "black";
1752 if (xs - ext.width / 2 > x1 &&
1753 xs + ext.width / 2 < x1 + width &&
1754 xs - ext.width / 2 > last_label_x + 2)
1755 ctx.fillText(str, xs, y1 + label);
1758 last_label_x = xs + ext.width / 2;
1765 /* supress 1.23E-17 ... */
1766 if (Math.abs(x_act) < dx / 100)
1773function vaxisDraw(ctx, x1, y1, height, line, minor, major, text, label, grid, ymin, ymax, logaxis) {
1774 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen, maxwidth;
1775 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
1776 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1779 ctx.textAlign = "right";
1781 ctx.textAlign = "left";
1782 ctx.textBaseline = "middle";
1783 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
1785 if (ymax <= ymin || height <= 0)
1789 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
1796 /* use 6 as min tick distance */
1797 dy = (ymax - ymin) / (height / 6);
1799 int_dy = Math.floor(Math.log(dy) / Math.log(10));
1800 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
1807 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
1808 major_base = label_base = tick_base + 1;
1810 /* rounding up of dy, label_dy */
1811 dy = Math.pow(10, int_dy) * base[tick_base];
1812 major_dy = Math.pow(10, int_dy) * base[major_base];
1813 label_dy = major_dy;
1815 /* number of significant digits */
1819 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1824 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1826 n_sig1 = Math.max(n_sig1, n_sig2);
1828 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1829 if (Math.abs(ymin) < 100000)
1830 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) + 1);
1831 if (Math.abs(ymax) < 100000)
1832 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) + 1);
1834 /* increase label_dy if labels would overlap */
1835 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
1837 label_dy = Math.pow(10, int_dy) * base[label_base];
1838 if (label_base % 3 === 2 && major_base % 3 === 1) {
1840 major_dy = Math.pow(10, int_dy) * base[major_base];
1852 y_act = Math.floor(ymin / dy) * dy;
1854 let last_label_y = y1;
1857 ctx.drawLine(x1, y1, x1, y1 - height);
1861 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) / (Math.log(ymax) - Math.log(ymin)) * height;
1863 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
1864 ys = Math.floor(y_screen + 0.5);
1866 if (y_screen < y1 - height - 0.001)
1869 if (y_screen <= y1 + 0.001) {
1870 if (Math.abs(Math.floor(y_act / major_dy + 0.5) - y_act / major_dy) <
1871 dy / major_dy / 10.0) {
1873 if (Math.abs(Math.floor(y_act / label_dy + 0.5) - y_act / label_dy) <
1874 dy / label_dy / 10.0) {
1875 /* label tick mark */
1876 ctx.drawLine(x1, ys, x1 + text, ys);
1879 if (grid != 0 && ys < y1 && ys > y1 - height)
1880 ctx.drawLine(x1, ys, x1 + grid, ys);
1884 str = y_act.toPrecision(n_sig1).stripZeros();
1886 ctx.fillStyle = "black";
1887 if (ys - textHeight / 2 > y1 - height &&
1888 ys + textHeight / 2 < y1)
1889 ctx.fillText(str, x1 + label, ys);
1891 last_label_y = ys - textHeight / 2;
1894 /* major tick mark */
1895 cts.drawLine(x1, ys, x1 + major, ys);
1898 if (grid != 0 && ys < y1 && ys > y1 - height)
1899 ctx.drawLine(x1, ys, x1 + grid, ys);
1909 /* minor tick mark */
1910 ctx.drawLine(x1, ys, x1 + minor, ys);
1912 /* for logaxis, also put labels on minor tick marks */
1915 str = y_act.toPrecision(n_sig1).stripZeros();
1917 ctx.fillStyle = "black";
1918 if (ys - textHeight / 2 > y1 - height &&
1919 ys + textHeight / 2 < y1 &&
1920 ys + textHeight < last_label_y + 2)
1921 ctx.fillText(str, x1 + label, ys);
1931 /* supress 1.23E-17 ... */
1932 if (Math.abs(y_act) < dy / 100)
1938function mhttpd_resize_sidenav() {
1939 //console.log("mhttpd_resize_sidenav!");
1940 let h = document.getElementById('mheader');
1941 let s = document.getElementById('msidenav');
1942 let top = h.clientHeight + 1 + "px";
1943 if (s.style.top !== top) {
1944 //console.log("httpd_resize_sidenav: top changed from " + s.style.top + " to " + top);
1947 let m = document.getElementById('mmain');
1948 let paddingTop = h.clientHeight + 1 + "px";
1949 if (m.style.paddingTop !== paddingTop) {
1950 //console.log("httpd_resize_sidenav: paddingTop changed from " + m.style.paddingTop + " to " + paddingTop);
1951 m.style.paddingTop = paddingTop;
1955function mhttpd_resize_header() {
1959let mhttpd_last_alarm = 0;
1961function get_options_path(odb_path) {
1962 // Convert "/Path/to/something" to "/Path/to/Options something"
1963 let bits = odb_path.split("/");
1964 bits[bits.length - 1] = "Options " + bits[bits.length - 1];
1965 return bits.join("/");
1968function evalPath(path) {
1970 // if path is a function, evaluate it
1971 if (p && !p.startsWith('/') && p.includes('(') && p.includes(')'))
1976function mhttpd_refresh() {
1978 if (!mhttpd_initialized)
1981 if (mhttpd_refresh_id !== undefined)
1982 window.clearTimeout(mhttpd_refresh_id);
1984 // don't do a refresh if we are paused
1985 if (mhttpd_refresh_paused) {
1986 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
1990 // don't update page if document is hidden (minimized, covered tab etc), only play alarms
1991 if (document.hidden) {
1993 // only check every 10 seconds for alarm
1994 if (new Date().getTime() > mhttpd_last_alarm + 10000) {
1996 // request current alarms
1997 let req = mjsonrpc_make_request("get_alarms");
1998 mjsonrpc_send_request([req]).then(function (rpc) {
2000 let alarms = rpc[0].result;
2002 // update alarm display
2003 if (alarms.alarm_system_active) {
2005 for (let a in alarms.alarms)
2009 mhttpd_alarm_play();
2012 }).catch(function (error) {
2013 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2014 mhttpd_error('Connection to server broken. Trying to reconnect ');
2015 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2016 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2018 mjsonrpc_error_alert(error);
2022 mhttpd_last_alarm = new Date().getTime();
2025 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, 500);
2029 // go through all modb* tags and refresh them -------------------------------------
2031 let modb = getMElements("modb");
2033 let modbvalue = getMElements("modbvalue");
2034 for (let i = 0; i < modbvalue.length; i++) {
2036 let o = modbvalue[i];
2037 if (parseInt(o.dataset.input)) {
2038 if (o.childNodes[0] === undefined) {
2039 ODBInlineEdit(o, evalPath(o.dataset.odbPath), 0);
2042 if (parseInt(o.dataset.odbEditable)) {
2043 if (o.childNodes[0] === undefined || o.childNodes[0].tagName === undefined) {
2045 // add link and event handler if tag is editable
2046 let link = document.createElement('a');
2048 link.onclick = function () {
2049 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2051 link.onfocus = function () {
2052 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2056 o.appendChild(link);
2060 // remove link if present
2061 if (o.childNodes[0] && o.childNodes[0].tagName === 'A')
2062 o.innerHTML = o.childNodes[0].innerHTML;
2066 modbvalue[i].setValue = function (x, tid) {
2067 if (this.dataset.formula !== undefined)
2068 x = eval(this.dataset.formula);
2069 if (tid === undefined)
2072 this.dataset.tid = tid;
2074 if (this.onchange !== null)
2076 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2078 this.dataset.odbLoaded = "1";
2082 modbvalue[i].sendValueToOdb = function () {
2083 mhttpd_refresh_pause(true);
2084 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2085 mhttpd_refresh_pause(false));
2089 let modbcheckbox = getMElements("modbcheckbox");
2090 for (let i = 0; i < modbcheckbox.length; i++) {
2092 // add event listener if missing
2093 if (!modbcheckbox[i].mEventListener) {
2094 modbcheckbox[i].addEventListener("click", function () {
2095 if (this.dataset.validate !== undefined) {
2096 let flag = eval(this.dataset.validate)(this.checked, this);
2102 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.checked ? 1 : 0).then(rpc =>
2105 modbcheckbox[i].mEventListener = true;
2109 let modbselect = getMElements("modbselect");
2110 for (let i = 0; i < modbselect.length; i++) {
2111 if (modbselect[i].dataset.odbPath) {
2113 // add event listener if missing
2114 if (!modbselect[i].mEventListener) {
2115 modbselect[i].addEventListener("change", function (flag) {
2117 if (flag !== true) { // flag === true if control changed by ODB update
2118 if (this.dataset.validate !== undefined) {
2119 let flag = eval(this.dataset.validate)(this.value, this);
2125 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(rpc =>
2129 modbselect[i].mEventListener = true;
2133 modbselect[i].setValue = function (x) {
2136 if (this.onchange !== null)
2140 modbselect[i].sendValueToOdb = function () {
2141 mhttpd_refresh_pause(true);
2142 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2143 mhttpd_refresh_pause(false));
2148 let modbbox = getMElements("modbbox");
2149 for (let i = 0; i < modbbox.length; i++) {
2150 modbbox[i].style.border = "1px solid #808080";
2153 // attach "set" function to all ODB buttons
2154 let modbbutton = getMElements("modbbutton");
2155 for (let i = 0; i < modbbutton.length; i++) {
2156 if (!modbbutton[i].mEventListener) {
2157 modbbutton[i].addEventListener("click", function () {
2159 if (this.dataset.validate !== undefined) {
2160 let flag = eval(this.dataset.validate)(this);
2167 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.dataset.odbValue).then(rpc =>
2170 modbbutton[i].mEventListener = true;
2174 // replace all horizontal bars with proper <div>'s
2175 let modbhbar = getMElements("modbhbar");
2176 for (let i = 0; i < modbhbar.length; i++) {
2177 if (modbhbar[i].childNodes === undefined || modbhbar[i].childNodes[0] === undefined ||
2178 modbhbar[i].childNodes[0].tagName !== 'DIV') {
2179 modbhbar[i].style.display = "block";
2180 if (modbhbar[i].style.position === "")
2181 modbhbar[i].style.position = "relative";
2182 modbhbar[i].style.border = "1px solid #808080";
2183 let color = modbhbar[i].style.color;
2184 modbhbar[i].innerHTML = "<div style='background-color:" + color + ";" + "color:black;" +
2185 "width:0;height:" + modbhbar[i].clientHeight + "px;" +
2186 "position:relative; display:inline-block;border-right:1px solid #808080'> </div>";
2188 modbhbar[i].setValue = function (x, tid) {
2189 if (this.dataset.formula !== undefined)
2190 x = eval(this.dataset.formula);
2191 if (tid === undefined)
2193 let mvalue = mie_to_string(tid, x, this.dataset.format);
2196 let html = mhttpd_escape(" " + mvalue);
2198 if (this.dataset.printValue === "1")
2199 this.children[0].innerHTML = html;
2200 let minValue = parseFloat(this.dataset.minValue);
2201 let maxValue = parseFloat(this.dataset.maxValue);
2202 if (isNaN(minValue))
2204 if (this.dataset.log === "1" &&
2207 if (isNaN(maxValue))
2210 if (this.dataset.log === "1")
2211 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2212 (Math.log(maxValue) - Math.log(minValue)));
2214 percent = Math.round(100 * (x - minValue) /
2215 (maxValue - minValue));
2220 this.children[0].style.width = percent + "%";
2221 if (this.onchange !== null)
2223 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2225 this.dataset.odbLoaded = "1";
2227 this.children[0].style.backgroundColor = this.style.color;
2232 // replace all vertical bars with proper <div>'s
2233 let modbvbar = getMElements("modbvbar");
2234 for (let i = 0; i < modbvbar.length; i++) {
2235 if (modbvbar[i].childNodes === undefined || modbvbar[i].childNodes[0] === undefined ||
2236 modbvbar[i].childNodes[0].tagName !== 'DIV') {
2237 modbvbar[i].style.display = "inline-block";
2238 if (modbvbar[i].style.position === "")
2239 modbvbar[i].style.position = "relative";
2240 modbvbar[i].style.border = "1px solid #808080";
2241 let color = modbvbar[i].style.color;
2242 modbvbar[i].innerHTML = "<div style='background-color:" + color + "; height:0; width:100%; position:absolute; bottom:0; left:0; display:inline-block; border-top:1px solid #808080'> </div>";
2244 modbvbar[i].setValue = function (x, tid) {
2245 if (this.dataset.formula !== undefined)
2246 x = eval(this.dataset.formula);
2247 if (tid === undefined)
2249 let mvalue = mie_to_string(tid, x, this.dataset.format);
2252 let html = mhttpd_escape(" " + mvalue);
2254 if (this.dataset.printValue === "1")
2255 this.children[0].innerHTML = html;
2256 let minValue = parseFloat(this.dataset.minValue);
2257 let maxValue = parseFloat(this.dataset.maxValue);
2258 if (isNaN(minValue))
2260 if (this.dataset.log === "1" &&
2263 if (isNaN(maxValue))
2266 if (this.dataset.log === "1")
2267 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2268 (Math.log(maxValue) - Math.log(minValue)));
2270 percent = Math.round(100 * (x - minValue) /
2271 (maxValue - minValue));
2276 this.children[0].style.height = percent + "%";
2277 if (this.onchange !== null)
2279 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2281 this.dataset.odbLoaded = "1";
2283 this.children[0].style.backgroundColor = this.style.color;
2288 // replace all thermometers with canvas
2289 let modbthermo = getMElements("modbthermo");
2290 for (let i = 0; i < modbthermo.length; i++) {
2291 if (modbthermo[i].childNodes === undefined || modbthermo[i].childNodes[0] === undefined ||
2292 modbthermo[i].childNodes[0].tagName !== 'CANVAS') {
2293 modbthermo[i].style.display = "inline-block";
2294 if (modbthermo[i].style.position === "")
2295 modbthermo[i].style.position = "relative";
2297 cvs = document.createElement("canvas");
2298 let w = modbthermo[i].clientWidth;
2299 let h = modbthermo[i].clientHeight;
2300 w = Math.floor(w / 4) * 4; // 2 must be devidable by 4
2303 modbthermo[i].appendChild(cvs);
2304 modbthermo[i].draw = mhttpd_thermo_draw;
2306 modbthermo[i].setValue = function (x, tid) {
2307 if (this.dataset.formula !== undefined)
2308 x = eval(this.dataset.formula);
2309 if (tid === undefined)
2312 this.dataset.tid = tid;
2314 if (this.onchange !== null)
2316 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2318 this.dataset.odbLoaded = "1";
2324 modbthermo[i].draw();
2328 // replace all gauges with canvas
2329 let modbgauge = getMElements("modbgauge");
2330 for (let i = 0; i < modbgauge.length; i++) {
2331 if (modbgauge[i].childNodes === undefined || modbgauge[i].childNodes[0] === undefined ||
2332 modbgauge[i].childNodes[0].tagName !== 'CANVAS') {
2333 modbgauge[i].style.display = "inline-block";
2334 if (modbgauge[i].style.position === "")
2335 modbgauge[i].style.position = "relative";
2337 let cvs = document.createElement("canvas");
2338 cvs.width = modbgauge[i].clientWidth;
2339 cvs.height = modbgauge[i].clientHeight;
2340 modbgauge[i].appendChild(cvs);
2341 modbgauge[i].draw = mhttpd_gauge_draw;
2343 modbgauge[i].setValue = function (x, tid) {
2344 if (this.dataset.formula !== undefined)
2345 x = eval(this.dataset.formula);
2346 if (tid === undefined)
2349 this.dataset.tid = tid;
2351 if (this.onchange !== null)
2353 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2355 this.dataset.odbLoaded = "1";
2361 modbgauge[i].draw();
2365 // replace all haxis with canvas
2366 let mhaxis = getMElements("mhaxis");
2367 for (let i = 0; i < mhaxis.length; i++) {
2368 if (mhaxis[i].childNodes === undefined || mhaxis[i].childNodes[0] === undefined ||
2369 mhaxis[i].childNodes[0].tagName !== 'CANVAS') {
2370 mhaxis[i].style.display = "block";
2371 if (mhaxis[i].style.position === "")
2372 mhaxis[i].style.position = "relative";
2374 let cvs = document.createElement("canvas");
2375 cvs.width = mhaxis[i].clientWidth + 2;
2376 cvs.height = mhaxis[i].clientHeight;
2377 mhaxis[i].appendChild(cvs);
2378 mhaxis[i].draw = mhttpd_haxis_draw;
2383 // replace all vaxis with canvas
2384 let mvaxis = getMElements("mvaxis");
2385 for (let i = 0; i < mvaxis.length; i++) {
2386 if (mvaxis[i].childNodes === undefined || mvaxis[i].childNodes[0] === undefined ||
2387 mvaxis[i].childNodes[0].tagName !== 'CANVAS') {
2388 mvaxis[i].style.display = "inline-block";
2389 if (mvaxis[i].style.position === "")
2390 mvaxis[i].style.position = "relative";
2392 let cvs = document.createElement("canvas");
2393 cvs.width = mvaxis[i].clientWidth;
2394 cvs.height = mvaxis[i].clientHeight + 2; // leave space for vbar border
2395 mvaxis[i].appendChild(cvs);
2396 mvaxis[i].draw = mhttpd_vaxis_draw;
2401 // replace all mhistory tags with history plots
2402 let mhist = getMElements("mhistory");
2403 for (let i = 0; i < mhist.length; i++) {
2404 if (mhist[i].childNodes === undefined || mhist[i].childNodes[0] === undefined ||
2405 mhist[i].childNodes[0].tagName !== 'IMG') {
2406 let w = mhist[i].style.width;
2411 let h = mhist[i].style.height;
2416 mhist[i].innerHTML = "<img src=\"graph.gif?cmd=oldhistory&group=" +
2417 mhist[i].dataset.group +
2418 "&panel=" + mhist[i].dataset.panel +
2419 "&scale=" + mhist[i].dataset.scale +
2422 "&rnd=" + (new Date().getTime()) +
2427 // request all modb* elements
2429 let modbs = ["modb", "modbvalue", "modbcheckbox", "modbselect", "modbbox", "modbhbar",
2430 "modbvbar", "modbthermo", "modbgauge"];
2432 modbs.forEach((mstr) => {
2433 getMElements(mstr).forEach((m) => {
2434 let p = m.dataset.odbPath;
2437 if (p.includes('[*]'))
2438 p = p.replace('[*]', '');
2442 if (mstr === "modbselect" && m.dataset.autoOptions === "1")
2443 paths.push(get_options_path(evalPath(m.dataset.odbPath)));
2448 // request ODB contents for all variables
2449 let req1 = mjsonrpc_make_request("db_get_values", {"paths": paths});
2451 // request current alarms
2452 let req2 = mjsonrpc_make_request("get_alarms");
2454 // request new messages
2455 let req3 = mjsonrpc_make_request("cm_msg_retrieve", {
2456 "facility": "midas",
2461 // request new char messages
2462 let req4 = mjsonrpc_make_request("cm_msg_retrieve", {
2468 mjsonrpc_send_request([req1, req2, req3, req4]).then(function (rpc) {
2470 // update time in header
2471 let dstr = mhttpd_get_display_time().string;
2473 if (document.getElementById("mheader_last_updated") !== undefined) {
2474 //mhttpd_set_innerHTML(document.getElementById("mheader_last_updated"), dstr);
2475 mhttpd_set_firstChild_data(document.getElementById("mheader_last_updated"), dstr);
2480 for (let i = 0; i < modb.length; i++, idata++) {
2481 if (modb[i].dataset.odbPath === undefined) {
2485 let x = rpc[0].result.data[idata];
2486 if (typeof x === 'object' && x !== null) {
2488 if (modb[i].value === undefined)
2489 modb[i].value = JSON.stringify(x);
2490 if (modb[i].onchange !== null && JSON.stringify(x) !== modb[i].value) {
2491 modb[i].value = JSON.stringify(x);
2494 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2496 modb[i].dataset.odbLoaded = "1";
2500 if (modb[i].value === undefined)
2502 if (modb[i].onchange !== null && x !== modb[i].value) {
2506 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2508 modb[i].dataset.odbLoaded = "1";
2513 for (let i = 0; i < modbvalue.length; i++, idata++) {
2514 if (modbvalue[i].dataset.odbPath === undefined) {
2519 if (rpc[0].result.status[idata] === 312) {
2520 modbvalue[i].innerHTML = "ODB key \"" +
2521 evalPath(modbvalue[i].dataset.odbPath) + "\" not found";
2523 let x = rpc[0].result.data[idata];
2524 let tid = rpc[0].result.tid[idata];
2526 modbvalue[i].tid = tid;
2527 if (modbvalue[i].dataset.formula !== undefined)
2528 x = eval(modbvalue[i].dataset.formula);
2529 let mvalue = mie_to_string(tid, x, modbvalue[i].dataset.format);
2532 else if (typeof mvalue === 'string' && mvalue.trim() === "")
2533 mvalue = "(spaces)";
2535 if (tid === TID_BOOL)
2538 html = mhttpd_escape(mvalue);
2539 if (parseInt(modbvalue[i].dataset.odbEditable) || parseInt(modbvalue[i].dataset.input)) {
2540 if (modbvalue[i].childNodes[1] !== undefined &&
2541 modbvalue[i].childNodes[1].innerHTML !== undefined) {
2542 modbvalue[i].childNodes[1].innerHTML = html;
2544 if (modbvalue[i].childNodes[0]) // dataset.input could not yet be initialized (AJAX)
2545 modbvalue[i].childNodes[0].innerHTML = html;
2548 modbvalue[i].innerHTML = html;
2550 if (modbvalue[i].value !== x && modbvalue[i].onchange !== null) {
2551 modbvalue[i].value = x;
2552 if (modbvalue[i].value !== undefined)
2553 modbvalue[i].onchange();
2556 modbvalue[i].value = x;
2558 if (modbvalue[i].dataset.odbLoaded === undefined && modbvalue[i].onload !== null) {
2559 modbvalue[i].onload();
2560 modbvalue[i].dataset.odbLoaded = "1";
2565 for (let i = 0; i < modbcheckbox.length; i++, idata++) {
2566 if (modbcheckbox[i].dataset.odbPath === undefined) {
2571 let x = rpc[0].result.data[idata];
2572 let tid = rpc[0].result.tid[idata];
2573 let mvalue = mie_to_string(tid, x);
2574 if (typeof x === "boolean")
2575 modbcheckbox[i].checked = x;
2577 modbcheckbox[i].checked = (mvalue !== "0");
2578 if (modbcheckbox[i].onchange !== null)
2579 modbcheckbox[i].onchange();
2580 if (modbcheckbox[i].dataset.odbLoaded === undefined && modbcheckbox[i].onload !== null) {
2581 modbcheckbox[i].onload();
2582 modbcheckbox[i].dataset.odbLoaded = "1";
2586 for (let i = 0; i < modbselect.length; i++, idata++) {
2587 if (modbselect[i].dataset.odbPath === undefined) {
2592 let x = rpc[0].result.data[idata];
2593 let auto_options = [];
2595 if (modbselect[i].dataset.autoOptions == "1") {
2597 auto_options = rpc[0].result.data[idata];
2599 if (auto_options !== null && modbselect[i].options.length == 0) {
2600 for (let opt = 0; opt < auto_options.length; opt++) {
2601 modbselect[i].add(new Option(auto_options[opt], auto_options[opt]));
2606 if (rpc[0].result.status[idata] === 312) {
2607 modbselect[i].parentElement.innerHTML = "ODB key \"" +
2608 evalPath(modbselect[i].dataset.odbPath) + "\" not found";
2610 if (x !== null && modbselect[i].value !== x.toString()) {
2612 if (modbselect[i].value !== undefined) {
2613 // check if option is available
2615 for (j = 0; j < modbselect[i].options.length; j++)
2616 if (modbselect[i].options[j].value === x.toString())
2619 // if option is available, set it
2620 if (j < modbselect[i].options.length) {
2621 modbselect[i].value = x.toString();
2622 if (modbselect[i].onchange !== null)
2623 modbselect[i].onchange(true);
2625 // if not, set blank
2626 if (modbselect[i].value !== "")
2627 modbselect[i].value = "";
2631 if (Array.isArray(x)) {
2632 // check if all array values are the same
2635 modbselect[i].value = s.values().next().value;
2637 modbselect[i].value = "...";
2640 if (modbselect[i].dataset.odbLoaded === undefined && modbselect[i].onload !== null) {
2641 modbselect[i].onload();
2642 modbselect[i].dataset.odbLoaded = "1";
2647 for (let i = 0; i < modbbox.length; i++, idata++) {
2648 if (modbbox[i].dataset.odbPath === undefined) {
2653 let x = rpc[0].result.data[idata];
2654 if (modbbox[i].dataset.formula !== undefined)
2655 x = eval(modbbox[i].dataset.formula);
2656 if (x > 0 || x === true) {
2657 modbbox[i].style.backgroundColor = modbbox[i].dataset.color;
2659 if (modbbox[i].dataset.backgroundColor !== undefined)
2660 modbbox[i].style.backgroundColor = modbbox[i].dataset.backgroundColor;
2662 modbbox[i].style.backgroundColor = "";
2664 if (modbbox[i].onchange !== null)
2665 modbbox[i].onchange();
2666 if (modbbox[i].dataset.odbLoaded === undefined && modbbox[i].onload !== null) {
2667 modbbox[i].onload();
2668 modbbox[i].dataset.odbLoaded = "1";
2672 for (let i = 0; i < modbhbar.length; i++, idata++) {
2673 if (modbhbar[i].dataset.odbPath === undefined) {
2678 let x = rpc[0].result.data[idata];
2679 let tid = rpc[0].result.tid[idata];
2681 modbhbar[i].setValue(x, tid);
2684 for (let i = 0; i < modbvbar.length; i++, idata++) {
2685 if (modbvbar[i].dataset.odbPath === undefined) {
2690 let x = rpc[0].result.data[idata];
2691 let tid = rpc[0].result.tid[idata];
2693 modbvbar[i].setValue(x, tid);
2696 for (let i = 0; i < modbthermo.length; i++, idata++) {
2697 if (modbthermo[i].dataset.odbPath === undefined) {
2702 let x = rpc[0].result.data[idata];
2703 let tid = rpc[0].result.tid[idata];
2705 modbthermo[i].setValue(x, tid);
2708 for (let i = 0; i < modbgauge.length; i++, idata++) {
2709 if (modbgauge[i].dataset.odbPath === undefined) {
2714 let x = rpc[0].result.data[idata];
2715 let tid = rpc[0].result.tid[idata];
2717 modbgauge[i].setValue(x, tid);
2720 let alarms = rpc[1].result;
2722 // update alarm display
2723 let e = document.getElementById('mheader_alarm');
2724 if (!alarms.alarm_system_active) {
2725 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: Off</a>");
2726 mhttpd_set_className(e, "mgray mbox");
2731 for (let a in alarms.alarms) {
2735 if (alarms.alarms[a].alarm_sound)
2739 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: None</a>");
2740 mhttpd_set_className(e, "mgreen mbox");
2744 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: " + s + "</a>");
2746 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarm: " + s + "</a>");
2747 mhttpd_set_className(e, "mred mbox");
2750 mhttpd_alarm_play();
2756 if (rpc[2].result.messages !== undefined) {
2757 msg = rpc[2].result.messages.split("\n");
2758 if (msg[msg.length - 1] === "")
2759 msg = msg.slice(0, -1);
2763 // update chat messages
2765 if (rpc[3].result.messages !== undefined) {
2766 chat = rpc[3].result.messages.split("\n");
2767 if (chat[chat.length - 1] === "")
2768 chat = chat.slice(0, -1);
2772 mhttpd_message(msg, chat);
2773 mhttpd_resize_sidenav();
2775 if (mhttpd_refresh_interval !== undefined && mhttpd_refresh_interval > 0)
2776 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2778 }).catch(function (error) {
2780 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2781 mhttpd_error('Connection to server broken. Trying to reconnect ');
2782 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2783 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2785 mjsonrpc_error_alert(error);
2790function mhttpd_refresh_history() {
2791 if (mhttpd_refresh_history_id !== undefined)
2792 window.clearTimeout(mhttpd_refresh_history_id);
2794 /* this fuction gets called by mhttpd_init to periodically refresh all history panels */
2796 let mhist = document.getElementsByName("mhistory");
2797 for (let i = 0; i < mhist.length; i++) {
2798 let s = mhist[i].childNodes[0].src;
2800 if (s.lastIndexOf("&rnd=") !== -1) {
2801 s = s.substr(0, s.lastIndexOf('&rnd='));
2802 s += "&rnd=" + new Date().getTime();
2804 mhist[i].childNodes[0].src = s;
2807 if (mhttpd_refresh_history_interval !== undefined && mhttpd_refresh_history_interval > 0)
2808 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2811function mhttpd_reconnect() {
2812 mjsonrpc_db_ls(["/"]).then(function (rpc) {
2813 // on successful connection remove error and schedule refresh
2814 mhttpd_error_clear();
2815 if (mhttpd_refresh_id !== undefined)
2816 window.clearTimeout(mhttpd_refresh_id);
2817 if (mhttpd_refresh_history_id !== undefined)
2818 window.clearTimeout(mhttpd_refresh_history_id);
2819 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2820 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2821 }).catch(function (error) {
2822 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2826window.addEventListener('resize', mhttpd_resize_message);
2828function mhttpd_resize_message() {
2829 //console.log("mhttpd_resize_message() via resize event listener");
2830 let d = document.getElementById("mheader_message");
2831 if (d.currentMessage !== undefined && d.style.display !== 'none')
2832 mhttpd_fit_message(d.currentMessage);
2835function mhttpd_close_message() {
2836 let d = document.getElementById("mheader_message");
2838 // remember time of messages to suppress
2839 mhttpdConfigSet('suppressMessageBefore', d.currentMessageT);
2841 d.style.display = "none";
2842 mhttpd_resize_sidenav();
2845function mhttpd_fit_message(m) {
2846 let d = document.getElementById("mheader_message");
2847 let cross = " <span style=\"cursor: pointer;\" onclick=\"mhttpd_close_message();\">╳</span>";
2848 let link1 = "<span style=\"cursor: pointer;\" onclick=\"window.location.href='?cmd=Messages'\">";
2849 let link2 = "</span>";
2850 d.style.display = "inline-block";
2852 // limit message to fit parent element
2854 let parentWidth = d.parentNode.offsetWidth;
2855 let maxWidth = parentWidth - 30;
2857 // check if the full message fits
2859 d.innerHTML = link1 + m + link2 + cross;
2860 //console.log("mhttpd_fit_message: len: " + d.offsetWidth + ", max: " + maxWidth + ", message: " + m);
2861 if (d.offsetWidth <= maxWidth) {
2865 // check if the message minus timestamp and type fits
2867 m = m.substr(m.indexOf(']')+1);
2868 d.innerHTML = link1 + m + link2 + cross;
2869 let w = d.offsetWidth;
2870 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + m);
2871 if (w <= maxWidth) {
2875 // guess the length assuming fix pixels per char
2877 let charWidth = w/m.length;
2878 let guessLength = maxWidth/charWidth - 3; // 3 chars of "..."
2880 let g = m.substr(0, guessLength);
2881 d.innerHTML = link1 + g + "..." + link2 + cross;
2883 //console.log("mhttpd_fit_message: char: " + charWidth + ", guess: " + guessLength + ", len: " + w + ", max: " + maxWidth);
2885 // grow or shrink our guess
2888 //console.log("mhttpd_fit_message: too short, grow");
2889 for (let i=guessLength+1; i<=m.length; i++) {
2890 let s = m.substr(0, i);
2891 d.innerHTML = link1 + s + "..." + link2 + cross;
2893 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + s);
2898 //console.log("mhttpd_fit_message: too long, shrink");
2899 while (g.length > 0) {
2900 g = g.substr(0, g.length-1);
2901 d.innerHTML = link1 + g + "..." + link2 + cross;
2903 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + g);
2910function mhttpd_message(msg, chat) {
2925 if (msg !== undefined) {
2926 lastMsg = msg[0].substr(msg[0].indexOf(" ") + 1);
2927 lastMsgT = parseInt(msg[0]);
2930 if (chat !== undefined) {
2931 lastChat = chat[0].substr(chat[0].indexOf(" ") + 1);
2932 lastChatT = parseInt(chat[0]);
2933 if (chat[0].length > 0)
2934 mTalk = chat[0].substr(chat[0].indexOf("]") + 2);
2936 chatName = lastChat.substring(lastChat.indexOf("[") + 1, lastChat.indexOf(","));
2937 lastChat = lastChat.substr(0, lastChat.indexOf("[")) +
2938 "<b>" + chatName + ":</b>" +
2939 lastChat.substr(lastChat.indexOf("]") + 1);
2942 if (lastChatT > lastMsgT) {
2946 talkTime = lastChatT;
2947 notifyTime = lastChatT;
2951 c = "var(--myellow)";
2952 mTalk = lastMsg.substr(lastMsg.indexOf("]") + 1);
2953 mType = m.substring(m.indexOf(",") + 1, m.indexOf("]"));
2954 talkTime = lastMsgT;
2955 notifyTime = lastMsgT;
2960 let d = document.getElementById("mheader_message");
2961 if (d !== undefined && d !== null && d.currentMessage !== m &&
2962 (mhttpdConfig().suppressMessageBefore === undefined || lastT > mhttpdConfig().suppressMessageBefore)) {
2964 d.style.removeProperty("-webkit-transition");
2965 d.style.removeProperty("transition");
2967 if (mType === "USER" && mhttpdConfig().displayChat ||
2968 mType === "TALK" && mhttpdConfig().displayTalk ||
2969 mType === "ERROR" && mhttpdConfig().displayError ||
2970 mType === "INFO" && mhttpdConfig().displayInfo ||
2971 mType === "LOG" && mhttpdConfig().displayLog) {
2973 let first = (d.currentMessage === undefined);
2974 d.currentMessage = m; // store full message in user-defined attribute
2975 d.currentMessageT = lastMsgT; // store message time in user-defined attribute
2977 mhttpd_fit_message(m);
2978 d.age = new Date() / 1000;
2981 if (m.search("ERROR]") > 0) {
2982 d.style.backgroundColor = "var(--mred)";
2983 d.style.color = "white";
2987 // manage backgroud color (red for errors, fading yellow for others)
2988 if (m.search("ERROR]") > 0) {
2989 d.style.removeProperty("-webkit-transition");
2990 d.style.removeProperty("transition");
2991 d.style.backgroundColor = "var(--mred)";
2993 d.age = new Date() / 1000;
2994 d.style.removeProperty("-webkit-transition");
2995 d.style.removeProperty("transition");
2996 d.style.backgroundColor = c;
3002 if (mType === "USER" && mhttpdConfig().speakChat) {
3003 // do not speak own message
3004 if (document.getElementById("chatName") === undefined ||
3005 document.getElementById("chatName") === null ||
3006 document.getElementById("chatName").value !== chatName) {
3007 mhttpd_speak(talkTime, mTalk);
3009 } else if (mType === "TALK" && mhttpdConfig().speakTalk) {
3010 mhttpd_speak(talkTime, mTalk);
3011 } else if (mType === "ERROR" && mhttpdConfig().speakError) {
3012 mhttpd_speak(talkTime, mTalk);
3013 } else if (mType === "INFO" && mhttpdConfig().speakInfo) {
3014 mhttpd_speak(talkTime, mTalk);
3015 } else if (mType === "LOG" && mhttpdConfig().speakLog) {
3016 mhttpd_speak(talkTime, mTalk);
3020 if (mType === "USER" && mhttpdConfig().notifyChat ||
3021 mType === "TALK" && mhttpdConfig().notifyTalk ||
3022 mType === "ERROR" && mhttpdConfig().notifyError ||
3023 mType === "INFO" && mhttpdConfig().notifyInfo ||
3024 mType === "LOG" && mhttpdConfig().notifyLog) {
3026 // avoid double notification if page gets reloaded
3027 // console.log("notifyTime: " + notifyTime + " last: " + mhttpdConfig().var.lastNotify);
3028 if (mhttpdConfig().var.lastNotify === undefined || notifyTime > mhttpdConfig().var.lastNotify) {
3029 mhttpdConfigSet("var.lastNotify", notifyTime);
3031 if (("Notification" in window)) {
3032 if (Notification.permission === "granted") {
3033 const n = new Notification("MIDAS", {
3035 icon: "./apple-touch-icon.png"
3037 } else if (Notification.permission !== "denied") {
3038 Notification.requestPermission().then((p) => {
3039 if (p === "granted") {
3040 const n = new Notification("MIDAS", {
3042 icon: "./apple-touch-icon.png"
3053 let t = new Date() / 1000;
3054 if (t > d.age + 5 && d.style.backgroundColor === "var(--myellow)") {
3055 let backgroundColor = "var(--mgray)";
3056 d.style.setProperty("-webkit-transition", "background-color 3s", "");
3057 d.style.setProperty("transition", "background-color 3s", "");
3058 d.style.backgroundColor = backgroundColor;
3063function mhttpd_error(error) {
3064 let d = document.getElementById("mheader_error");
3065 if (d !== undefined) {
3066 error += "<div style=\"display: inline; float: right; padding-right: 10px; cursor: pointer;\"" +
3067 " onclick=\"document.getElementById("mheader_error").style.zIndex = 0;\">╳</div>";
3068 d.innerHTML = error;
3069 d.style.zIndex = 3; // above header
3073function mhttpd_error_clear() {
3074 if (document.getElementById("mheader_error")) {
3075 document.getElementById("mheader_error").innerHTML = "";
3076 document.getElementById("mheader_error").style.zIndex = 0; // below header
3080function mhttpd_create_page_handle_create(mouseEvent) {
3084 let arraylength = "";
3085 let stringlength = "";
3087 let form = document.getElementsByTagName('form')[0];
3090 path = form.elements['odb'].value;
3091 type = form.elements['type'].value;
3092 name = form.elements['value'].value;
3093 arraylength = form.elements['index'].value;
3094 stringlength = form.elements['strlen'].value;
3096 let e = document.getElementById("odbpath");
3097 path = JSON.parse(e.innerHTML);
3098 if (path === "/") path = "";
3100 type = document.getElementById("create_tid").value;
3101 name = document.getElementById("create_name").value;
3102 arraylength = document.getElementById("create_array_length").value;
3103 stringlength = document.getElementById("create_strlen").value;
3105 //alert("Path: " + path + " Name: " + name);
3108 if (path === "/") path = "";
3110 if (name.length < 1) {
3111 dlgAlert("Name is too short");
3115 let int_array_length = parseInt(arraylength);
3117 //alert("int_array_length: " + int_array_length);
3119 if (!int_array_length || int_array_length < 1) {
3120 dlgAlert("Bad array length: " + arraylength);
3124 let int_string_length = parseInt(stringlength);
3126 if (!int_string_length || int_string_length < 1) {
3127 dlgAlert("Bad string length " + stringlength);
3132 param.path = path + "/" + name;
3133 param.type = parseInt(type);
3134 if (int_array_length > 1)
3135 param.array_length = int_array_length;
3136 if (int_string_length > 0)
3137 param.string_length = int_string_length;
3139 mjsonrpc_db_create([param]).then(function (rpc) {
3140 let status = rpc.result.status[0];
3141 if (status === 311) {
3142 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3143 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3145 } else if (status !== 1) {
3146 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages.", true, true, function () {
3147 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3150 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3152 }).catch(function (error) {
3153 mjsonrpc_error_alert(error);
3154 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3160function mhttpd_create_page_handle_cancel(mouseEvent) {
3161 dlgHide('dlgCreate');
3165function mhttpd_link_page_handle_link(mouseEvent) {
3166 let e = document.getElementById("link_odbpath");
3167 let path = JSON.parse(e.innerHTML);
3168 if (path === "/") path = "";
3169 //let path = document.getElementById("odb_path").value;
3170 let name = document.getElementById("link_name").value;
3171 let target = document.getElementById("link_target").value;
3173 //console.log("Path: " + path + " Name: " + name + " Target: [" + target + "]");
3175 if (name.length < 1) {
3176 dlgAlert("Name is too short");
3180 if (target.length <= 1) {
3181 dlgAlert("Link target is too short");
3186 param.new_links = [path + "/" + name];
3187 param.target_paths = [target];
3189 mjsonrpc_call("db_link", param).then(function (rpc) {
3190 let status = rpc.result.status[0];
3191 if (status === 304) {
3192 dlgMessage("Error", "Invalid link, see MIDAS messages.", true, true, function () {
3193 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3195 } else if (status === 311) {
3196 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3197 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3199 } else if (status === 312) {
3200 dlgMessage("Error", "Target path " + target + " does not exist in ODB.", true, true, function () {
3201 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3203 } else if (status === 315) {
3204 dlgMessage("Error", "ODB data type mismatch, see MIDAS messages.", true, true, function () {
3205 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3207 } else if (status !== 1) {
3208 dlgMessage("Error", "db_create_link() error " + status + ", see MIDAS messages.", true, true, function () {
3209 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3212 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3214 }).catch(function (error) {
3215 mjsonrpc_error_alert(error);
3216 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3222function mhttpd_link_page_handle_cancel(mouseEvent) {
3227function mhttpd_delete_page_handle_delete(mouseEvent, xpath) {
3228 let form = document.getElementsByTagName('form')[0];
3233 path = form.elements['odb'].value;
3235 if (path === "/") path = "";
3237 for (let i = 0; ; i++) {
3239 let v = form.elements[n];
3240 if (v === undefined)
3243 names.push(path + "/" + v.value);
3246 let e = document.getElementById("odbpath");
3247 path = JSON.parse(e.innerHTML);
3248 if (path === "/") path = "";
3250 //alert("Path: " + path);
3252 for (i = 0; ; i++) {
3253 let v = document.getElementById("delete" + i);
3254 if (v === undefined || v === null)
3257 let name = JSON.parse(v.value);
3258 if (name.length > 0) {
3259 names.push(path + "/" + name);
3264 //alert("Names: " + names);
3268 if (names.length < 1) {
3269 dlgAlert("Please select at least one ODB entry to delete.");
3276 params.paths = names;
3277 mjsonrpc_call("db_delete", params).then(function (rpc) {
3279 let status = rpc.result.status;
3280 //alert(JSON.stringify(status));
3281 for (let i = 0; i < status.length; i++) {
3282 if (status[i] !== 1) {
3283 message += "Cannot delete \"" + rpc.request.params.paths[i] + "\", db_delete_key() status " + status[i] + "\n";
3286 if (message.length > 0) {
3289 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3291 }).catch(function (error) {
3292 mjsonrpc_error_alert(error);
3293 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3299function mhttpd_delete_page_handle_cancel(mouseEvent) {
3300 dlgHide('dlgDelete');
3304function mhttpd_start_run(ret) {
3305 mhttpd_goto_page("Start", ret); // DOES NOT RETURN
3308function mhttpd_stop_run(ret) {
3309 dlgConfirm('Are you sure to stop the run?', function (flag) {
3310 if (flag === true) {
3311 mjsonrpc_call("cm_transition", {"transition": "TR_STOP"}).then(function (rpc) {
3312 //mjsonrpc_debug_alert(rpc);
3313 if (rpc.result.status !== 1) {
3314 throw new Error("Cannot stop run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3316 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3317 }).catch(function (error) {
3318 mjsonrpc_error_alert(error);
3325function mhttpd_pause_run(ret) {
3326 dlgConfirm('Are you sure to pause the run?', function (flag) {
3327 if (flag === true) {
3328 mjsonrpc_call("cm_transition", {"transition": "TR_PAUSE"}).then(function (rpc) {
3329 //mjsonrpc_debug_alert(rpc);
3330 if (rpc.result.status !== 1) {
3331 throw new Error("Cannot pause run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3333 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3334 }).catch(function (error) {
3335 mjsonrpc_error_alert(error);
3342function mhttpd_resume_run(ret) {
3343 dlgConfirm('Are you sure to resume the run?', function (flag) {
3344 if (flag === true) {
3345 mjsonrpc_call("cm_transition", {"transition": "TR_RESUME"}).then(function (rpc) {
3346 //mjsonrpc_debug_alert(rpc);
3347 if (rpc.result.status !== 1) {
3348 throw new Error("Cannot resume run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3350 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3351 }).catch(function (error) {
3352 mjsonrpc_error_alert(error);
3358function mhttpd_cancel_transition() {
3359 dlgConfirm('Are you sure to cancel the currently active run transition?', function (flag) {
3360 if (flag === true) {
3364 paths.push("/Runinfo/Requested Transition");
3366 paths.push("/Runinfo/Transition in progress");
3370 params.paths = paths;
3371 params.values = values;
3373 mjsonrpc_call("db_paste", params).then(function (rpc) {
3374 //mjsonrpc_debug_alert(rpc);
3375 if ((rpc.result.status[0] !== 1) || (rpc.result.status[1] !== 1)) {
3376 throw new Error("Cannot cancel transition, db_paste() status " + rpc.result.status + ", see MIDAS messages");
3378 mhttpd_goto_page("Transition"); // DOES NOT RETURN
3379 }).catch(function (error) {
3380 mjsonrpc_error_alert(error);
3386function mhttpd_reset_alarm(alarm_name) {
3387 mjsonrpc_call("al_reset_alarm", {"alarms": [alarm_name]}).then(function (rpc) {
3388 //mjsonrpc_debug_alert(rpc);
3389 if (rpc.result.status[0] !== 1 && rpc.result.status[0] !== 1004) {
3390 throw new Error("Cannot reset alarm, status " + rpc.result.status + ", see MIDAS messages");
3392 }).catch(function (error) {
3393 mjsonrpc_error_alert(error);
3397/*---- site and session storage ----------------------------*/
3402 flag = mhttpdConfig().speakChat; // read
3404 mhttpdConfigSet('speakChat', false); // write individual config
3406 let c = mhttpdConfig(); // write whole config
3407 c.speakChat = false;
3409 mhttpdConfigSetAll(c);
3412 Saves settings are kept in local storage, which gets
3413 cleared when the browser session ends. Then the default
3414 values are returned.
3417let mhttpd_config_defaults = {
3425 'displayChat': true,
3426 'displayTalk': true,
3427 'displayError': true,
3428 'displayInfo': false,
3429 'displayLog': false,
3433 'speakError': false,
3437 'displayChat': true,
3438 'displayTalk': true,
3439 'displayError': true,
3440 'displayInfo': true,
3441 'displayLog': false,
3443 'speakVoice': 'Alex',
3447 'alarmSoundFile': 'beep.mp3',
3451 'timezone': 'local',
3458 'suppressMessageBefore': 0,
3461 'facility': 'midas',
3466function mhttpdConfigODB(callback) {
3467 let c = mhttpd_config_defaults;
3469 if (localStorage.mhttpd) {
3470 c = JSON.parse(localStorage.mhttpd);
3473 // obtain some defaults from ODB
3474 mjsonrpc_db_get_value("/Experiment/Enable sound").then(function (rpc) {
3475 let flag = rpc.result.data[0];
3476 if (flag === null) {
3477 mjsonrpc_db_create([{"path" : "/Experiment/Enable sound", "type" : TID_BOOL}]).then(function (rpc) {
3478 mjsonrpc_db_paste(["/Experiment/Enable sound"],[true]).then(function(rpc) {}).catch(function(error) {
3479 mjsonrpc_error_alert(error); });
3480 }).catch(function (error) {
3481 mjsonrpc_error_alert(error);
3483 } else if (flag === false) {
3484 c.speakChat = false;
3485 c.speakTalk = false;
3486 c.speakError = false;
3487 c.speakInfo = false;
3489 c.alarmSound = false;
3490 localStorage.setItem('mhttpd', JSON.stringify(c));
3493 }).catch(function (error) {
3494 mjsonrpc_error_alert(error);
3501function mhttpdConfig() {
3502 let c = mhttpd_config_defaults;
3504 if (localStorage.mhttpd)
3505 c = JSON.parse(localStorage.mhttpd);
3507 // if element has been added to mhttpd_config_defaults, merge it
3508 if (Object.keys(c).length !== Object.keys(mhttpd_config_defaults).length) {
3509 for (let o in mhttpd_config_defaults)
3511 c[o] = mhttpd_config_defaults[o];
3519function mhttpdConfigSet(item, value) {
3521 let c = mhttpdConfig();
3522 if (item.indexOf('.') > 0) {
3523 let c1 = item.substring(0, item.indexOf('.'));
3524 let c2 = item.substring(item.indexOf('.') + 1);
3528 localStorage.setItem('mhttpd', JSON.stringify(c));
3533function mhttpdConfigSetAll(new_config) {
3535 localStorage.setItem('mhttpd', JSON.stringify(new_config));
3540/*---- sound and speak functions --------------------------*/
3542let last_audio = null;
3543let inside_new_audio = false;
3547// Do not delete the following code. It is not used at this moment,
3548// but may become necessary in the future. Included
3549// is an alternative method of allocating and releasing Audio objects
3550// and code for tracing Audio object life time and activity. It is needed
3551// to debug interactions between Audio objects and throttled javascript code
3552// in inactive tabs; and with the "user did not interact with this tab" business.
3555//function mhttpd_alarm_done() {
3558// ended = last_audio.ended;
3559// last_audio = null;
3561// count_audio_done++;
3562// console.log(Date() + ": mhttpd_alarm_done: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended);
3565//function mhttpd_audio_loadeddata(e) {
3566// console.log(Date() + ": mhttpd_audio_loadeddata: counter " + e.target.counter);
3569//function mhttpd_audio_canplay(e) {
3570// console.log(Date() + ": mhttpd_audio_canplay: counter " + e.target.counter);
3573//function mhttpd_audio_canplaythrough(e) {
3574// console.log(Date() + ": mhttpd_audio_canplaythrough: counter " + e.target.counter);
3577//function mhttpd_audio_ended(e) {
3578// console.log(Date() + ": mhttpd_audio_ended: counter " + e.target.counter);
3581//function mhttpd_audio_paused(e) {
3582// console.log(Date() + ": mhttpd_audio_paused: counter " + e.target.counter);
3585function mhttpd_alarm_play_now() {
3589 // check for playing the alarm sound is done every few minutes.
3590 // it takes 3 seconds to play the alarm sound
3591 // so in theory, this check is not needed: previous alarm sound should
3592 // always have finished by the time we play a new sound.
3594 // However, observed with google-chrome, in inactive and/or iconized tabs
3595 // javascript code is "throttled" and the above time sequence is not necessarily
3598 // Specifically, I see the following sequence: after 1-2 days of inactivity,
3599 // (i.e.) the "programs" page starts consuming 10-15% CPU, if I open it,
3600 // CPU use goes to 100% for a short while, memory use goes from ~50 Mbytes
3601 // to ~150 Mbytes. console.log() debug print out shows that there are ~400
3602 // accumulated Audio() objects that have been created but did not finish playing
3603 // (because google-chrome is throttling javascript in inactive tabs?), and now
3604 // they all try to load the mp3 file and play all at the same time.
3606 // This fix for this observed behaviour is to only create new Audio() objects
3607 // if previous one finished playing.
3611 if (!last_audio.ended) {
3612 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: previous alarm sound did not finish playing yet");
3617 if (inside_new_audio) {
3618 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: already inside \"new Audio()\"");
3621 inside_new_audio = true;
3623 // reference counting of Audio objects. Not in use today. Do not remove. K.O. Jan 2021
3624 //console.log(Date() + ": mhttpd_alarm_play: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended + ", audio.play!");
3625 //count_audio_created++;
3627 let audio = new Audio(mhttpdConfig().alarmSoundFile);
3628 audio.volume = mhttpdConfig().alarmVolume;
3629 audio.counter = ++count_audio;
3632 inside_new_audio = false;
3634 // code for tracing activity of Audio objects. do not remove. K.O. Jan 2021
3635 //audio.addEventListener("loadeddata", mhttpd_audio_loadeddata);
3636 //audio.addEventListener("canplay", mhttpd_audio_canplay);
3637 //audio.addEventListener("canplaythrough", mhttpd_audio_canplaythrough);
3638 //audio.addEventListener("ended", mhttpd_audio_ended);
3639 //audio.addEventListener("paused", mhttpd_audio_paused);
3641 let promise = audio.play();
3643 promise.then(function(e) {
3644 //console.log(Date() + ": mhttpd_alarm_play: promise fulfilled, counter " + audio.counter);
3645 }).catch(function(e) {
3646 //count_audio_done++;
3647 //console.log(Date() + ": mhttpd_alarm_play: audio.play() exception: " + e + ", created: " + count_audio_created + ", done: " + count_audio_done);
3648 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: audio.play() exception: " + e + ", counter " + audio.counter);
3649 // NB: must clear the URL of the sound file, otherwise, the sound file is still loaded (but not played)
3650 // the loading of sound files is observed to be delayed in inactive tabs
3651 // resulting in many (100-1000) pending loads getting queued, observed
3652 // to all of them attempt to run (load the sound file) in parallel,
3653 // consuming memory (100-300 Mbytes) and CPU (10-15% CPU per inactive tab).
3655 // NB: setting audio to pause() does not seem to do anything.
3663function mhttpd_alarm_play() {
3664 if (mhttpdConfig().alarmSound && mhttpdConfig().alarmSoundFile) {
3665 let now = new Date() / 1000;
3666 let last = mhttpdConfig().var.lastAlarm;
3667 let next = last + parseFloat(mhttpdConfig().alarmRepeat);
3668 let wait = next - now;
3669 let do_play = (now > next);
3670 //console.log("mhttpd_alarm_play: now: " + now + ", next: " + next + ", last: " + last + ", wait: " + wait + ", play: " + do_play);
3672 mhttpdConfigSet("var.lastAlarm", now);
3673 mhttpd_alarm_play_now();
3678function mhttpd_speak_now(text) {
3679 let u = new SpeechSynthesisUtterance(text);
3680 u.voice = speechSynthesis.getVoices().filter(function (voice) {
3681 return voice.name === mhttpdConfig().speakVoice;
3683 u.volume = mhttpdConfig().speakVolume;
3684 speechSynthesis.speak(u);
3687function mhttpd_speak(time, text) {
3689 if (!('speechSynthesis' in window))
3692 if (mhttpdConfig().speakChat) {
3693 if (time > mhttpdConfig().var.lastSpeak) {
3694 mhttpdConfigSet("var.lastSpeak", time);
3695 mhttpd_speak_now(text);
3704 * js-indent-level: 3
3705 * indent-tabs-mode: nil