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]);
282 setTimeout(function () {
284 p.removeChild(p.childNodes[0]);
290 if (p.value !== value) {
293 if (p.onchange !== null) {
298 }).catch(function (error) {
299 mjsonrpc_error_alert(error);
304// called from ODBFinishInlineEdit if element has 'confirm' flag
306function ODBConfirmInlineEdit(flag, p) {
309 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
310 value = p.childNodes[1].value;
312 value = p.childNodes[0].value;
315 mjsonrpc_db_set_value(p.odbPath, value).then(function (rpc) {
316 if (parseInt(p.dataset.input) !== 1) {
318 mie_back_to_link(p, p.odbPath);
320 }).catch(function (error) {
321 mjsonrpc_error_alert(error);
324 if (parseInt(p.dataset.input) !== 1) {
326 mie_back_to_link(p, p.odbPath);
333// odb inline edit - write new value to odb
335function ODBFinishInlineEdit(p, path) {
338 if (p.ODBsent === true)
344 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
345 value = p.childNodes[1].value;
347 value = p.childNodes[0].value;
349 //console.log("mie_write odb [" + path + "] value [" + value + "]");
351 // check for validation
352 if (p.dataset.validate !== undefined) {
353 let flag = eval(p.dataset.validate)(value, p);
355 if (parseInt(p.dataset.input) !== 1) {
358 mie_back_to_link(p, path);
364 // validator might have changed the value
365 if (p.childNodes[1] !== undefined && p.childNodes[1].value !== undefined)
366 value = p.childNodes[1].value;
368 value = p.childNodes[0].value;
371 if (p.dataset.confirm !== undefined) {
373 dlgConfirm(p.dataset.confirm, ODBConfirmInlineEdit, p);
377 mjsonrpc_db_set_value(path, value).then(function (rpc) {
378 if (parseInt(p.dataset.input) !== 1) {
381 mie_back_to_link(p, path);
383 }).catch(function (error) {
384 mjsonrpc_error_alert(error);
389// odb inline edit - key-press handler
391function ODBInlineEditKeydown(event, p, path) {
392 let keyCode = ('which' in event) ? event.which : event.keyCode;
394 if (keyCode === 27) {
396 if (parseInt(p.dataset.input) !== 1) {
399 mie_back_to_link(p, path);
404 if (keyCode === 13) {
405 ODBFinishInlineEdit(p, path);
413// odb inline edit - convert link to edit field
415function mie_link_to_edit(p, cur_val, size) {
416 let string_val = String(cur_val);
419 if (p.tagName === 'A')
420 p.oldStyle = p.style;
421 else if (p.getElementsByTagName('a').length > 0)
422 p.oldStyle = p.getElementsByTagName('a')[0].style;
424 if (size === undefined)
426 let str = mhttpd_escape(string_val);
429 if (p.innerHTML[0] === '[' && p.innerHTML.indexOf(']') !== 0)
430 index = p.innerHTML.substring(0, p.innerHTML.indexOf(']')+1) + " ";
432 p.innerHTML = index + "<input type='text' size='" + size + "' value='" + str +
433 "' onKeydown='return ODBInlineEditKeydown(event, this.parentNode,"" +
434 p.odbPath + ""," + ");' onBlur='ODBFinishInlineEdit(this.parentNode,"" +
435 p.odbPath + ""," + ");' >";
437 // needed for Firefox
438 if (parseInt(p.dataset.input) !== 1)
439 setTimeout(function () {
441 p.childNodes[0].focus();
442 p.childNodes[0].select();
444 p.childNodes[1].focus();
445 p.childNodes[1].select();
451// odb inline edit - start editing
453function ODBInlineEdit(p, odb_path) {
457 p.odbPath = odb_path;
459 mjsonrpc_db_get_values([odb_path]).then(function (rpc) {
460 let value = rpc.result.data[0];
461 if (Array.isArray(value))
463 let tid = rpc.result.tid[0];
464 let format = p.dataset.format;
467 let size = p.dataset.size;
468 if (size === undefined) {
470 if (size === undefined || size < 20)
474 if (format.length > 1) {
475 if (format[0] === 'd' || format[0] === 'x' || format[0] === 'b') {
476 //when going to edit consider only the first format specifier for integers
477 format = String(format[0]);
481 // JSON parser can't cope with locale-based strings (thousands separators), so
482 // force the user to edit the value as a regular number.
486 let mvalue = mie_to_string(tid, value, format);
487 mie_link_to_edit(p, mvalue, size);
488 }).catch(function (error) {
489 mjsonrpc_error_alert(error);
493function dlgOdbEditKeydown(event, input) {
494 let keyCode = ('which' in event) ? event.which : event.keyCode;
496 if (keyCode === 27) {
498 dlgMessageDestroy(input.parentElement.parentElement);
502 if (keyCode === 13) {
503 dlgOdbEditSend(input.parentElement);
504 dlgMessageDestroy(input.parentElement.parentElement);
511function dlgOdbEditSend(b) {
512 let path = b.parentElement.parentElement.parentElement.odbPath;
513 let value = b.parentElement.parentElement.elements[0].value;
515 mjsonrpc_db_set_value(path, value).then(function (rpc) {
516 //mjsonrpc_debug_alert(rpc);
517 }).catch(function (error) {
518 mjsonrpc_error_alert(error);
523// odb edit single value dialog box
525function dlgOdbEdit(path) {
527 mjsonrpc_db_get_value(path).then(function (rpc) {
528 let value = rpc.result.data[0];
529 let tid = rpc.result.tid[0];
530 value = mie_to_string(tid, value);
532 d = document.createElement("div");
533 d.className = "dlgFrame";
537 d.innerHTML = "<div class=\"dlgTitlebar\" id=\"dlgMessageTitle\">Change ODB value</div>" +
539 "<div class=\"dlgPanel\" style=\"padding: 30px;\">" +
540 "<div id=\"dlgMessageString\">" +
541 path + " : " +
542 "<input type='text' size='16' value='" + value + "' onkeydown='return dlgOdbEditKeydown(event, this);'>" +
545 "<button class=\"dlgButton\" id=\"dlgMessageButton\" style=\"background-color:#F8F8F8\" type=\"button\" " +
546 " onClick=\"dlgOdbEditSend(this);dlgMessageDestroy(this.parentElement);\">Ok</button>" +
547 "<button class=\"dlgButton\" id=\"dlgMessageButton\" style=\"background-color:#F8F8F8\" type=\"button\" " +
548 " onClick=\"dlgMessageDestroy(this.parentElement)\">Cancel</button>" +
552 document.body.appendChild(d);
556 // needed for Firefox
557 setTimeout(function () {
558 d.childNodes[1].elements[0].focus();
559 d.childNodes[1].elements[0].select();
563 }).catch(function (error) {
564 mjsonrpc_error_alert(error);
568/*---- mhttpd functions -------------------------------------*/
571// Returns time to display in status bar and history.
572// By default show local time, but through the config page the
573// user can select the server time zone or any other time zone
575function mhttpd_get_display_time(sec) {
576 // retrieve timezone of server on the first call
577 if (serverTimezoneOffset === undefined) {
578 mjsonrpc_get_timezone().then(function (rpc) {
579 serverTimezoneOffset = rpc.result;
580 }).catch(function (error) {
581 mjsonrpc_error_alert(error);
586 if (sec !== undefined)
587 d = new Date(sec * 1000);
592 if (mhttpdConfig().timezone === undefined)
593 mhttpdConfigSet('timezone', 'local');
595 if (mhttpdConfig().timezone === 'local') {
596 o = -d.getTimezoneOffset() / 60;
597 } else if (mhttpdConfig().timezone === 'server') {
598 if (serverTimezoneOffset === undefined)
604 o = serverTimezoneOffset / 3600;
606 o = parseInt(mhttpdConfig().timezone);
609 d = new Date(d.getTime() + o * 3600 * 1000);
611 let s = d.toLocaleString("en-gb", {
613 hour12: false, day: 'numeric', month: 'short', year: 'numeric',
614 hour: 'numeric', minute: 'numeric', second: 'numeric'
631function mhttpd_disable_button(button) {
632 if (!button.disabled) {
633 button.disabled = true;
637function mhttpd_enable_button(button) {
638 if (button.disabled) {
639 button.disabled = false;
643function mhttpd_set_style_visibility(e, v) {
645 if (e.style.visibility !== v) {
646 e.style.visibility = v;
651function mhttpd_set_style_display(e, v) {
653 if (e.style.display !== v) {
659function mhttpd_set_firstChild_data(e, v) {
661 if (e.firstChild.data !== v) {
662 //console.log("mhttpd_set_firstChild_data for " + e + " from " + e.firstChild.data + " to " + v);
663 e.firstChild.data = v;
668function mhttpd_set_innerHTML(e, v) {
670 if (e.innerHTML !== v) {
671 //console.log("mhttpd_set_innerHTML for " + e + " from " + e.innerHTML + " to " + v);
677function mhttpd_set_className(e, v) {
679 if (e.className !== v) {
680 //console.log("mhttpd_set_className for " + e + " from " + e.className + " to " + v);
686function mhttpd_hide_button(button) {
687 mhttpd_set_style_visibility(button, "hidden");
688 mhttpd_set_style_display(button, "none");
691function mhttpd_unhide_button(button) {
692 mhttpd_set_style_visibility(button, "visible");
693 mhttpd_set_style_display(button, "");
696function mhttpd_hide(id) {
697 let e = document.getElementById(id);
699 mhttpd_set_style_visibility(e, "hidden");
700 mhttpd_set_style_display(e, "none");
704function mhttpd_unhide(id) {
705 let e = document.getElementById(id);
707 mhttpd_set_style_visibility(e, "visible");
708 mhttpd_set_style_display(e, "");
712function mhttpd_enable(id) {
713 let e = document.getElementById(id);
718function mhttpd_disable(id) {
719 let e = document.getElementById(id);
724function mhttpd_init_overlay(overlay) {
725 mhttpd_hide_overlay(overlay);
727 // this element will hide the underlaying web page
729 overlay.style.zIndex = 10;
730 //overlay.style.backgroundColor = "rgba(0,0,0,0.5)"; /*dim the background*/
731 overlay.style.backgroundColor = "white";
732 overlay.style.position = "fixed";
733 overlay.style.top = "0%";
734 overlay.style.left = "0%";
735 overlay.style.width = "100%";
736 overlay.style.height = "100%";
738 return overlay.children[0];
741function mhttpd_hide_overlay(overlay) {
742 overlay.style.visibility = "hidden";
743 overlay.style.display = "none";
746function mhttpd_unhide_overlay(overlay) {
747 overlay.style.visibility = "visible";
748 overlay.style.display = "";
751function mhttpd_getParameterByName(name) {
752 let match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
753 return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
756function mhttpd_goto_page(page, param) {
757 if (!param) param = "";
758 window.location.href = '?cmd=' + page + param; // reloads the page from new URL
762function mhttpd_navigation_bar(current_page, path) {
763 document.write("<div id='customHeader'>\n");
764 document.write("</div>\n");
766 document.write("<div class='mnav'>\n");
767 document.write(" <table>\n");
768 document.write(" <tr><td id='navigationTableButtons'></td></tr>\n");
769 document.write(" </table>\n\n");
770 document.write("</div>\n");
775 if (localStorage.mNavigationButtons !== undefined) {
776 document.getElementById("navigationTableButtons").innerHTML = localStorage.mNavigationButtons;
777 let button = document.getElementById("navigationTableButtons").children;
778 for (let i = 0; i < button.length; i++)
779 if (button[i].value.toLowerCase() === current_page.toLowerCase())
780 button[i].className = "mnav mnavsel navButtonSel";
782 button[i].className = "mnav navButton";
786 mjsonrpc_db_get_values(["/Custom/Header", "/Experiment/Menu", "/Experiment/Menu Buttons"]).then(function (rpc) {
787 let custom_header = rpc.result.data[0];
789 if (custom_header && custom_header.length > 0)
790 document.getElementById("customHeader").innerHTML = custom_header;
792 let menu = rpc.result.data[1];
793 let buttons = rpc.result.data[2];
797 for (let k in menu) {
798 let kk = k + "/name";
805 } else if (buttons && buttons.length > 0) {
806 b = buttons.split(",");
809 if (!b || b.length < 1) {
810 b = ["Status", "ODB", "Messages", "Chat", "ELog", "Alarms", "Programs", "History", "MSCB", "Sequencer", "Config", "Help"];
815 for (let i = 0; i < b.length; i++) {
816 let bb = b[i].trim();
818 let cc = "mnav navButton";
819 if (bb.toLowerCase() === current_page.toLowerCase()) {
820 cc = "mnav mnavsel navButtonSel";
822 if (bb == "PySequencer") {
823 cmd = "Sequencer&SeqODB=/PySequencer";
825 html += "<input type=button name=cmd value='" + bb + "' class='" + cc + "' onclick='window.location.href=\'" + path + "?cmd=" + cmd + "\';return false;'>\n";
827 document.getElementById("navigationTableButtons").innerHTML = html;
829 // cache navigation buttons in browser local storage
830 localStorage.setItem("mNavigationButtons", html);
832 }).catch(function (error) {
833 mjsonrpc_error_alert(error);
837function mhttpd_show_menu(flag) {
838 let m = document.getElementById("msidenav");
840 if (m.initialWidth === undefined)
841 m.initialWidth = m.clientWidth;
844 m.style.width = m.initialWidth + "px";
845 document.getElementById("mmain").style.marginLeft = m.initialWidth + "px";
846 mhttpdConfigSet('hideMenu', false);
849 document.getElementById("mmain").style.marginLeft = "0";
852 mhttpdConfigSet('showMenu', flag);
855function mhttpd_toggle_menu() {
856 let flag = mhttpdConfig().showMenu;
858 mhttpd_show_menu(flag);
861function mhttpd_exec_script(name) {
862 //console.log("exec_script: " + name);
863 let params = new Object;
864 params.script = name;
865 mjsonrpc_call("exec_script", params).then(function (rpc) {
866 let status = rpc.result.status;
868 dlgAlert("Exec script \"" + name + "\" status " + status);
870 }).catch(function (error) {
871 mjsonrpc_error_alert(error);
875let mhttpd_refresh_id;
876let mhttpd_refresh_history_id;
877let mhttpd_refresh_interval;
878let mhttpd_refresh_paused;
879let mhttpd_refresh_history_interval;
880let mhttpd_refresh_boost;
881let mhttpd_spinning_wheel;
882let mhttpd_initialized = false;
884function mhttpd_init(current_page, interval, callback) {
886 This function should be called from custom pages to initialize all ODB tags and refresh
887 them periodically every "interval" in ms
889 For possible ODB tags please refer to
891 https://midas.triumf.ca/MidasWiki/index.php/New_Custom_Pages_(2017)
894 if (mhttpd_initialized) {
895 dlgAlert("mhttpd_init() is called more than once from user code");
900 url = mhttpd_getParameterByName("URL");
902 mjsonrpc_set_url(url);
904 // retrieve current page if not given
905 if (current_page === undefined) {
906 current_page = mhttpd_getParameterByName("page");
908 if (current_page === undefined || current_page === null) {
909 dlgAlert("Please specify name of page via mhttpd_init('<name>')<br>or make sure to have '&page=<name>' in URL");
914 let h = document.getElementById("mheader");
916 dlgAlert('Web page does not contain "mheader" element');
919 let s = document.getElementById("msidenav");
921 dlgAlert('Web page does not contain "msidenav" element');
925 mhttpd_initialized = true;
927 // set global font size
928 let config = mhttpdConfig();
929 document.body.style.fontSize = config.fontSize + "pt";
931 h.style.display = "flex";
933 "<div style='display:inline-block; flex:none;'>" +
934 "<span class='mmenuitem' style='padding: 5px;'><a href='https://midas.triumf.ca'>" +
935 "<img alt='MIDAS home page' src='favicon.png'></a></span>" +
936 "<span class='mmenuitem' style='padding: 5px;margin-right: 20px;' onclick='mhttpd_toggle_menu()'>☰</span>" +
937 "<span id='mheader_expt_name'></span>" +
940 "<div style='flex:auto;'>" +
941 " <div id='mheader_message'></div>" +
944 "<div style='display: inline; flex:none;'>" +
945 " <div id='mheader_alarm'> </div>" +
946 " <div style='display: inline; font-size: 75%; margin-right: 10px' id='mheader_last_updated'>mheader_last_updated</div>" +
949 mhttpd_resize_sidenav();
950 window.addEventListener('resize', mhttpd_resize_sidenav);
952 // put error header in front of header
953 let d = document.createElement('div');
954 d.id = 'mheader_error';
955 h.parentNode.insertBefore(d, h);
957 // update header and menu
958 if (document.getElementById("msidenav") !== undefined) {
960 // get it from session storage cache if present
961 if (sessionStorage.msidenav !== undefined && sessionStorage.mexpname !== undefined) {
962 let menu = document.getElementById("msidenav");
963 menu.innerHTML = sessionStorage.msidenav;
964 let item = menu.children;
965 for (let i = 0; i < item.length; i++) {
966 if (item[i].className !== "mseparator") {
967 if (current_page.search(item[i].innerHTML) !== -1)
968 item[i].className = "mmenuitem mmenuitemsel";
970 item[i].className = "mmenuitem";
973 document.getElementById("mheader_expt_name").innerHTML = sessionStorage.mexpname;
975 // now the side navigation has its full width, adjust the main body and make it visible
976 let m = document.getElementById("mmain");
977 if (m !== undefined) {
978 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
979 m.style.opacity = "1";
983 // request it from server, since it might have changed
984 mjsonrpc_db_get_values(["/Experiment/Name", "/Experiment/Menu", "/Experiment/Menu Buttons",
985 "/Custom", "/Script", "/Alias"]).then(function (rpc) {
987 let expt_name = rpc.result.data[0];
988 let menu = rpc.result.data[1];
989 let buttons = rpc.result.data[2];
990 let custom = rpc.result.data[3];
991 let script = rpc.result.data[4];
992 let alias = rpc.result.data[5];
994 document.getElementById("mheader_expt_name").innerHTML = expt_name;
995 sessionStorage.setItem("mexpname", expt_name);
997 // preload spinning wheel for later use
998 if (mhttpd_spinning_wheel === undefined) {
999 mhttpd_spinning_wheel = new Image();
1000 mhttpd_spinning_wheel.src = "spinning-wheel.gif";
1006 for (let k in menu) {
1007 if (k.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1009 if (menu[k]) // show button if not disabled
1010 b.push(menu[k + "/name"]);
1012 } else if (buttons && buttons.length > 0) {
1013 b = buttons.split(",");
1016 if (!b || b.length < 1) {
1017 b = ["Status", "ODB", "Messages", "Chat", "ELog", "Alarms", "Programs", "History", "MSCB", "Sequencer", "Config", "Help"];
1022 for (let i = 0; i < b.length; i++) {
1023 let bb = b[i].trim();
1024 let cc = "mmenuitem";
1026 if (bb === current_page) {
1027 cc += " mmenuitemsel";
1029 if (bb == "PySequencer") {
1030 cmd = "Sequencer&SeqODB=/PySequencer";
1032 html += "<div class='" + cc + "'><a href='?cmd=" + cmd + "' class='mmenulink'>" + bb + "</a></div>\n";
1036 if (custom !== null && Object.keys(custom).length > 0) {
1038 html += "<div class='mseparator'></div>\n";
1039 // add menu items recursively
1040 html = mhttpd_add_menu_items(html, custom, current_page, "", 0);
1044 if (script !== null && Object.keys(script).length > 0) {
1046 html += "<div class='mseparator'></div>\n";
1048 for (let b in script) {
1049 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1051 let n = script[b + "/name"];
1052 //html += "<div class='mmenuitem'><a href='?script=" + b + "' class='mmenulink'>" + n + "</a></div>\n";
1053 html += "<div class='mmenuitem'><button class='mbutton' onclick='mhttpd_exec_script(\"" + n + "\")'>" + n + "</button></div>\n";
1059 if (alias !== null && Object.keys(alias).length > 0) {
1061 html += "<div class='mseparator'></div>\n";
1064 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1066 n = alias[b + "/name"];
1067 if (n.substr(n.length - 1) === "&") {
1068 n = n.substr(0, n.length - 1);
1069 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink' target='_blank'>" + n + "↗</a></div>\n";
1071 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink'>" + n + "</a></div>\n";
1077 // dummy spacer to fix scrolling to the bottom, must be at least header height
1078 html += "<div style='height: 64px;'></div>\n";
1080 document.getElementById("msidenav").innerHTML = html;
1082 // re-adjust size of mmain element if menu has changed
1083 let m = document.getElementById("mmain");
1084 if (m !== undefined) {
1085 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
1086 m.style.opacity = "1";
1089 // cache navigation buttons in browser local storage
1090 sessionStorage.setItem("msidenav", html);
1092 // show/hide sidenav according to local storage settings
1093 mhttpd_show_menu(mhttpdConfig().showMenu);
1095 // hide all invisible menu items after layout has been finished
1096 let mi = document.getElementsByClassName('mmenuitem');
1098 let p = m.parentElement;
1099 if (p.style.visibility === "hidden") {
1100 p.style.visibility = "visible";
1101 p.style.display = "none";
1104 mi = document.getElementsByClassName('msubmenuitem');
1106 let p = m.parentElement;
1107 if (p.style.visibility === "hidden") {
1108 p.style.visibility = "visible";
1109 p.style.display = "none";
1113 }).then(function () {
1114 if (callback !== undefined)
1116 }).catch(function (error) {
1117 mjsonrpc_error_alert(error);
1121 // add event handler to tabs
1122 const tabs = document.querySelectorAll('.mtab');
1123 const panels = document.querySelectorAll('.mpanel');
1125 tabs.forEach(tab => {
1126 tab.addEventListener('click', () => {
1127 const id = tab.dataset.target;
1129 tabs.forEach(t => t.classList.toggle('active', t === tab));
1130 panels.forEach(p => p.classList.toggle('active', p.id === id));
1134 // store refresh interval and do initial refresh
1135 if (interval === undefined)
1137 mhttpd_refresh_interval = interval;
1138 mhttpd_refresh_paused = false;
1140 // history interval is static for now
1141 mhttpd_refresh_history_interval = 30000;
1143 // trigger first refresh
1145 mhttpd_refresh_history();
1148function mhttpd_find_menuitem(current_page, submenu) {
1149 for (let b in submenu) {
1151 if (typeof submenu[b] === "object") { // recurse into submenu
1152 let flag = mhttpd_find_menuitem(current_page, submenu[b]);
1157 if (typeof submenu[b] === "string") {
1159 if (s[s.length-1] === '&')
1162 if (b.indexOf('/name') !== -1 && s.toLowerCase() === current_page.toLowerCase())
1171function mhttpd_add_menu_items(html, custom, current_page, path, level) {
1172 for (let b in custom) {
1173 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1175 if (b === "path" || b === "images" || b === "default") // skip "path", "Images" and "default"
1178 if (typeof custom[b] === "object") { // submenu created by subdirectory in ODB
1179 let cc = "msubmenuitem";
1181 let expand = mhttpd_find_menuitem(current_page, custom[b]);
1183 // add blanks according to nesting level
1184 for (let i=0 ; i<level ; i++)
1185 l += " ";
1188 l += "▾ "; // caret down
1190 l += "▸ "; // caret right
1193 l += custom[b + "/name"];
1199 html += "<div class='" + cc + "' onclick='mhttpd_submenu(this)'><div class='mmenulink'>" + l + "</div></div>\n";
1202 html += "<div>"; // do not hide submenu if current page is under it
1204 html += "<div style='visibility: hidden'>";
1205 html = mhttpd_add_menu_items(html, custom[b], current_page, p, level+1);
1207 } else if (typeof custom[b] === "string") { // skip any items that don't have type of string, since can't be valid links
1208 let cc = "mmenuitem";
1209 let mitem = custom[b + "/name"];
1210 if (mitem.slice(-1) === '&')
1211 mitem = mitem.slice(0, -1);
1212 if (current_page.search(mitem) !== -1 ||
1213 current_page.search(mitem.toLowerCase()) !== -1)
1214 cc += " mmenuitemsel";
1216 for (let i = 0; i < level; i++)
1217 ln += " ";
1218 ln += custom[b + "/name"];
1220 if (custom[b].substring(0, 5) === "link:") {
1221 let lt = custom[b].substring(5);
1222 html += "<div class='" + cc + "'><a href='" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1227 lt += custom[b + "/name"];
1228 if (ln.slice(-1) === '!')
1232 let clicklink = false;
1234 // style = "style='display: none'";
1236 // Accepted file extentions, if not create onclick call from value
1237 const validExtensionsRegex = /\.([a-zA-Z0-9]+)$/; // /\.(txt|jpg|jpeg|png|gif|pdf|csv|html|htm|js|css)$/i;
1239 if (ln.slice(-1) === '&') {
1240 ln = ln.slice(0, -1);
1241 lt = lt.slice(0, -1);
1242 target += "target='_blank' ";
1243 } else if (!validExtensionsRegex.test(custom[b])) {
1248 const handler = custom[b]
1249 .replace(/&/g, "&") // must do ampersand first
1250 .replace(/"/g, """)
1251 .replace(/'/g, "'");
1252 html += `<div ${style} class="mmenuitem" onclick="${handler}"><a style="width: 100%;display: inline-block;text-decoration: none;color: #404040;" class="mmenulink">${ln}</a></div>\n`;
1254 html += "<div class='" + cc + "' " + style + "><a " + target + " href='?cmd=custom&page=" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1264function mhttpd_submenu(o) {
1266 for (i=0 ; o.firstChild.innerHTML.charCodeAt(i) !== 9656 && o.firstChild.innerHTML.charCodeAt(i) !== 9662 ; i++);
1267 if (o.firstChild.innerHTML.charCodeAt(i) === 9656) {
1269 o.firstChild.innerHTML =
1270 o.firstChild.innerHTML.substring(0, i-1) +
1271 String.fromCharCode(9662) +
1272 o.firstChild.innerHTML.substring(i+1);
1273 o.nextElementSibling.style.display = "inline";
1274 o.nextElementSibling.style.visibility = "visible";
1277 o.firstChild.innerHTML =
1278 o.firstChild.innerHTML.substring(0, i-1) +
1279 String.fromCharCode(9656) +
1280 o.firstChild.innerHTML.substring(i+1);
1281 o.nextElementSibling.style.display = "none";
1285function mhttpd_refresh_pause(flag) {
1286 mhttpd_refresh_paused = flag;
1289function mhttpd_set_refresh_interval(interval) {
1290 mhttpd_refresh_interval = interval;
1293function getMElements(name) {
1294 // collect all <div name=[name] >
1296 e.push(...document.getElementsByName(name));
1298 // collect all <div class=[name] >
1299 e.push(...document.getElementsByClassName(name));
1304function mhttpd_thermo_draw() {
1306 if (this.value === undefined)
1309 let ctx = this.firstChild.getContext("2d");
1312 let w = this.firstChild.width;
1313 let h = this.firstChild.height;
1315 ctx.clearRect(0, 0, w, h);
1316 w = w - 1; // space for full circles
1319 if (this.dataset.scale === "1") {
1321 w = Math.floor(w / 4) * 4;
1324 if (this.dataset.printValue === "1") {
1328 let x0 = Math.round(w / 4 * 0);
1329 let x1 = Math.round(w / 4 * 1);
1330 let x2 = Math.round(w / 4 * 2);
1331 let x3 = Math.round(w / 4 * 3);
1334 if (v < this.dataset.minValue)
1335 v = this.dataset.minValue;
1336 if (v > this.dataset.maxValue)
1337 v = this.dataset.maxValue;
1338 let yt = (h - 4 * x1) - (h - 5 * x1) * (v - this.dataset.minValue) / (this.dataset.maxValue - this.dataset.minValue);
1340 ctx.translate(0.5, 0.5);
1341 ctx.strokeStyle = "#000000";
1342 ctx.fillStyle = "#FFFFFF";
1347 ctx.arc(x2, x1, x1, Math.PI, 0);
1348 ctx.lineTo(x3, h - x1 * 4);
1349 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1350 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1351 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1354 if (this.dataset.backgroundColor !== undefined) {
1355 ctx.fillStyle = this.dataset.backgroundColor;
1360 if (this.dataset.color === undefined) {
1361 ctx.strokeStyle = "#000000";
1362 ctx.fillStyle = "#000000";
1364 ctx.strokeStyle = this.dataset.color;
1365 ctx.fillStyle = this.dataset.color;
1369 ctx.moveTo(x1 + 3, yt);
1370 ctx.lineTo(x3 - 3, yt);
1371 ctx.lineTo(x3 - 3, h - x2);
1372 ctx.lineTo(x1 + 3, h - x2);
1373 ctx.lineTo(x1 + 3, yt);
1378 ctx.arc(x2, h - x2, x2 - 4, 0, 2 * Math.PI);
1382 // re-draw outer "glass"
1383 ctx.strokeStyle = "#000000";
1384 ctx.fillStyle = "#FFFFFF";
1387 ctx.arc(x2, x1, x1, Math.PI, 0);
1388 ctx.lineTo(x3, h - x1 * 4);
1389 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1390 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1391 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1396 if (this.dataset.scale === "1") {
1398 ctx.moveTo(x3 + x1 / 2, x1);
1399 ctx.lineTo(x3 + x1, x1);
1400 ctx.moveTo(x3 + x1 / 2, h - 4 * x1);
1401 ctx.lineTo(x3 + x1, h - 4 * x1);
1404 ctx.font = "12px sans-serif";
1405 ctx.fillStyle = "#000000";
1406 ctx.strokeStyle = "#000000";
1407 ctx.textBaseline = "middle";
1409 let mintext = this.dataset.minValue;
1410 let maxtext = this.dataset.maxValue;
1412 if (this.dataset.format !== undefined) {
1413 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1414 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1417 ctx.fillText(mintext, 4.5 * x1, h - 4 * x1, 3.5 * x1);
1418 ctx.fillText(maxtext, 4.5 * x1, x1, 3.5 * x1);
1421 // optional value display
1422 if (this.dataset.printValue === "1") {
1423 ctx.font = "12px sans-serif";
1424 ctx.fillStyle = "#000000";
1425 ctx.strokeStyle = "#000000";
1426 ctx.textBaseline = "bottom";
1427 ctx.textAlign = "center";
1429 let valuetext = this.value;
1431 if (this.dataset.format !== undefined) {
1432 valuetext = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1435 ctx.fillText(valuetext, x2, this.firstChild.height, 4 * x1);
1441function mhttpd_gauge_draw() {
1443 if (this.value === undefined)
1446 let ctx = this.firstChild.getContext("2d");
1448 let w = this.firstChild.width;
1449 let h = this.firstChild.height;
1451 if (this.dataset.scale === "1")
1455 ctx.clearRect(0, 0, w, h);
1458 if (v < Number(this.dataset.minValue))
1459 v = Number(this.dataset.minValue);
1460 if (v > Number(this.dataset.maxValue))
1461 v = Number(this.dataset.maxValue);
1462 v = (v - Number(this.dataset.minValue)) /
1463 (Number(this.dataset.maxValue) - Number(this.dataset.minValue));
1465 ctx.translate(0.5, 0.5);
1466 ctx.strokeStyle = "#000000";
1467 ctx.fillStyle = "#FFFFFF";
1470 let gaugeColor = parseColorSpec(this.dataset.color); // can be simple color or array of pairs
1473 if (typeof gaugeColor === "string") {
1476 ctx.arc(w / 2, y, w / 2 - 1, Math.PI, 0);
1477 ctx.lineTo(w - 4, y);
1478 ctx.arc(w / 2, y, w / 2 - 4, 0, Math.PI, true);
1480 ctx.fillStyle = gaugeColor;
1486 for (let i=0 ; i<gaugeColor.length ; i++) {
1487 let startValue = gaugeColor[i].value;
1488 let startAngle = Math.PI + Math.PI * (startValue - Number(this.dataset.minValue)) /
1489 (Number(this.dataset.maxValue) - Number(this.dataset.minValue));
1491 let endValue = i+1 < gaugeColor.length ? gaugeColor[i+1].value : Number(this.dataset.maxValue);
1492 let endAngle = Math.PI + Math.PI * (endValue - Number(this.dataset.minValue)) /
1493 (Number(this.dataset.maxValue) - Number(this.dataset.minValue));
1495 let rOuter = w / 2 - 1;
1496 let rInner = rOuter - 3;
1498 // leave little gaps between colors
1499 if (startAngle > Math.PI)
1501 if (endAngle < Math.PI * 2)
1505 ctx.arc(w / 2, y, rOuter, startAngle, endAngle);
1506 ctx.lineTo(w / 2 + rInner * Math.cos(endAngle), y + rInner * Math.sin(endAngle));
1507 ctx.arc(w / 2, y, rInner, endAngle, startAngle, true);
1508 ctx.lineTo(w / 2 + rOuter * Math.cos(startAngle), y + rOuter * Math.sin(startAngle));
1509 ctx.fillStyle = gaugeColor[i].color;
1516 ctx.fillStyle = this.dataset.backgroundColor ? this.dataset.backgroundColor : "#E0E0E0";
1517 ctx.arc(w / 2, y, w / 2 - 8, Math.PI, 2*Math.PI);
1518 ctx.arc(w / 2, y, w / 2 - w / 5, 2*Math.PI, Math.PI, true);
1522 if (typeof gaugeColor === "string") {
1525 ctx.fillStyle = gaugeColor;
1526 ctx.arc(w / 2, y, w / 2 - 8, Math.PI, (1 + v) * Math.PI);
1527 ctx.arc(w / 2, y, w / 2 - w / 5, (1 + v) * Math.PI, Math.PI, true);
1533 for (i=0 ; i<gaugeColor.length-1 ; i++) {
1534 if (this.value >= gaugeColor[i].value && this.value < gaugeColor[i+1].value)
1539 ctx.fillStyle = gaugeColor[i].color;
1540 ctx.arc(w / 2, y, w / 2 - 8, Math.PI, (1 + v) * Math.PI);
1541 ctx.arc(w / 2, y, w / 2 - w / 5, (1 + v) * Math.PI, Math.PI, true);
1547 ctx.strokeStyle = "#404040";
1549 ctx.moveTo(w / 2 + (w/2-1) * Math.cos((1 + v) * Math.PI), y + (w/2-1) * Math.sin((1 + v) * Math.PI));
1550 ctx.lineTo(w / 2 + (w/2-w/5)* Math.cos((1 + v) * Math.PI), y + (w/2-w/5) * Math.sin((1 + v) * Math.PI));
1554 if (this.dataset.frame) {
1555 ctx.strokeStyle = "#000000";
1558 ctx.arc(w / 2, y, w / 2 - 6, Math.PI, 0);
1559 ctx.lineTo(w - w / 5, y);
1560 ctx.arc(w / 2, y, w / 2 - w / 5, 0, Math.PI, true);
1565 // optional value display
1566 if (this.dataset.printValue === "1") {
1567 ctx.font = "12px sans-serif";
1568 ctx.fillStyle = "#000000";
1569 ctx.strokeStyle = "#000000";
1570 ctx.textBaseline = "bottom";
1571 ctx.textAlign = "center";
1573 let valText = this.value;
1575 if (this.dataset.format !== undefined)
1576 valText = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1578 ctx.fillText(valText, w / 2, y, 0.5 * w);
1581 // optional scale display
1582 if (this.dataset.scale === "1") {
1583 ctx.font = "12px sans-serif";
1584 ctx.fillStyle = "#000000";
1585 ctx.strokeStyle = "#000000";
1586 ctx.textBaseline = "bottom";
1587 ctx.textAlign = "center";
1589 let mintext = this.dataset.minValue;
1590 let maxtext = this.dataset.maxValue;
1592 if (this.dataset.format !== undefined) {
1593 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1594 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1597 ctx.fillText(mintext, 0.1 * w, h, 0.2 * w);
1598 ctx.fillText(maxtext, 0.9 * w, h, 0.2 * w);
1604function mhttpd_vaxis_draw() {
1605 let ctx = this.firstChild.getContext("2d");
1607 let w = this.firstChild.width;
1608 let h = this.firstChild.height;
1609 ctx.clearRect(0, 0, w, h);
1612 if (this.dataset.line === "0")
1615 if (this.dataset.log === "1")
1620 if (this.dataset.minValue !== undefined)
1621 scaleMin = parseFloat(this.dataset.minValue);
1622 if (log && scaleMin === 0)
1624 if (this.dataset.maxValue !== undefined)
1625 scaleMax = parseFloat(this.dataset.maxValue);
1626 if (scaleMin === scaleMax)
1629 ctx.translate(0.5, 0.5);
1630 ctx.strokeStyle = "#000000";
1631 ctx.fillStyle = "#FFFFFF";
1634 if (this.style.textAlign === "left") {
1635 ctx.translate(-0.5, -0.5);
1636 vaxisDraw(ctx, 0, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1638 ctx.translate(-0.5, -0.5);
1639 vaxisDraw(ctx, w, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1645function mhttpd_haxis_draw() {
1646 let ctx = this.firstChild.getContext("2d");
1648 let w = this.firstChild.width;
1649 let h = this.firstChild.height;
1650 ctx.clearRect(0, 0, w, h);
1653 if (this.dataset.line === "0")
1656 if (this.dataset.log === "1")
1661 if (this.dataset.minValue !== undefined)
1662 scaleMin = parseFloat(this.dataset.minValue);
1663 if (log && scaleMin === 0)
1665 if (this.dataset.maxValue !== undefined)
1666 scaleMax = parseFloat(this.dataset.maxValue);
1667 if (scaleMin === scaleMax)
1670 ctx.translate(-0.5, 0);
1671 ctx.strokeStyle = "#000000";
1672 ctx.fillStyle = "#FFFFFF";
1675 if (this.style.verticalAlign === "top") {
1676 ctx.translate(0.5, 0.5);
1677 haxisDraw(ctx, 1, 0, w - 2, line, 4, 8, 10, 10, 0, scaleMin, scaleMax, log);
1679 ctx.translate(0.5, -0.5);
1680 haxisDraw(ctx, 1, h, w - 2, line, 4, 8, 10, 20, 0, scaleMin, scaleMax, log);
1685String.prototype.stripZeros = function () {
1686 let s = this.trim();
1687 if (s.search("[.]") >= 0) {
1688 let i = s.search("[e]");
1690 while (s.charAt(i - 1) === "0") {
1691 s = s.substring(0, i - 1) + s.substring(i);
1694 if (s.charAt(i - 1) === ".")
1695 s = s.substring(0, i - 1) + s.substring(i);
1697 while (s.charAt(s.length - 1) === "0")
1698 s = s.substring(0, s.length - 1);
1699 if (s.charAt(s.length - 1) === ".")
1700 s = s.substring(0, s.length - 1);
1706function haxisDraw(ctx, x1, y1, width, line, minor, major, text, label, grid, xmin, xmax, logaxis) {
1707 let dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
1708 let tick_base, major_base, label_base, n_sig1, n_sig2, xs;
1709 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1711 ctx.textAlign = "center";
1712 ctx.textBaseline = "top";
1714 if (xmax <= xmin || width <= 0)
1718 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1725 /* use 6 as min tick distance */
1726 dx = (xmax - xmin) / (width / 6);
1728 int_dx = Math.floor(Math.log(dx) / Math.log(10));
1729 frac_dx = Math.log(dx) / Math.log(10) - int_dx;
1736 tick_base = frac_dx < (Math.log(2) / Math.log(10)) ? 1 : frac_dx < (Math.log(5) / Math.log(10)) ? 2 : 3;
1737 major_base = label_base = tick_base + 1;
1739 /* rounding up of dx, label_dx */
1740 dx = Math.pow(10, int_dx) * base[tick_base];
1741 major_dx = Math.pow(10, int_dx) * base[major_base];
1742 label_dx = major_dx;
1745 /* number of significant digits */
1749 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1754 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1756 n_sig1 = Math.max(n_sig1, n_sig2);
1758 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1759 if (Math.abs(xmin) < 100000)
1760 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) + 1);
1761 if (Math.abs(xmax) < 100000)
1762 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) + 1);
1764 /* determination of maximal width of labels */
1765 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
1766 let ext = ctx.measureText(str);
1767 maxwidth = ext.width;
1769 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
1770 ext = ctx.measureText(str);
1771 maxwidth = Math.max(maxwidth, ext.width);
1772 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
1773 maxwidth = Math.max(maxwidth, ext.width);
1775 /* increasing label_dx, if labels would overlap */
1776 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
1778 label_dx = Math.pow(10, int_dx) * base[label_base];
1779 if (label_base % 3 === 2 && major_base % 3 === 1) {
1781 major_dx = Math.pow(10, int_dx) * base[major_base];
1796 x_act = Math.floor(xmin / dx) * dx;
1798 let last_label_x = x1;
1801 ctx.drawLine(x1, y1, x1 + width, y1);
1805 x_screen = (Math.log(x_act) - Math.log(xmin)) / (Math.log(xmax) - Math.log(xmin)) * width + x1;
1807 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
1808 xs = Math.floor(x_screen + 0.5);
1810 if (x_screen > x1 + width + 0.001)
1813 if (x_screen >= x1) {
1814 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
1815 dx / major_dx / 10.0) {
1817 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
1818 dx / label_dx / 10.0) {
1819 /* label tick mark */
1820 ctx.drawLine(xs, y1, xs, y1 + text);
1823 if (grid != 0 && xs > x1 && xs < x1 + width)
1824 ctx.drawLine(xs, y1, xs, y1 + grid);
1828 str = x_act.toPrecision(n_sig1).stripZeros();
1829 ext = ctx.measureText(str);
1831 ctx.fillStyle = "black";
1832 if (xs - ext.width / 2 > x1 &&
1833 xs + ext.width / 2 < x1 + width)
1834 ctx.fillText(str, xs, y1 + label);
1836 last_label_x = xs + ext.width / 2;
1839 /* major tick mark */
1840 ctx.drawLine(xs, y1, xs, y1 + major);
1843 if (grid != 0 && xs > x1 && xs < x1 + width)
1844 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
1853 /* minor tick mark */
1854 ctx.drawLine(xs, y1, xs, y1 + minor);
1856 /* for logaxis, also put labes on minor tick marks */
1859 str = x_act.toPrecision(n_sig1).stripZeros();
1860 ext = ctx.measureText(str);
1862 ctx.fillStyle = "black";
1863 if (xs - ext.width / 2 > x1 &&
1864 xs + ext.width / 2 < x1 + width &&
1865 xs - ext.width / 2 > last_label_x + 2)
1866 ctx.fillText(str, xs, y1 + label);
1869 last_label_x = xs + ext.width / 2;
1876 /* supress 1.23E-17 ... */
1877 if (Math.abs(x_act) < dx / 100)
1884function vaxisDraw(ctx, x1, y1, height, line, minor, major, text, label, grid, ymin, ymax, logaxis) {
1885 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen, maxwidth;
1886 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
1887 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1890 ctx.textAlign = "right";
1892 ctx.textAlign = "left";
1893 ctx.textBaseline = "middle";
1894 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
1896 if (ymax <= ymin || height <= 0)
1900 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
1907 /* use 6 as min tick distance */
1908 dy = (ymax - ymin) / (height / 6);
1910 int_dy = Math.floor(Math.log(dy) / Math.log(10));
1911 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
1918 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
1919 major_base = label_base = tick_base + 1;
1921 /* rounding up of dy, label_dy */
1922 dy = Math.pow(10, int_dy) * base[tick_base];
1923 major_dy = Math.pow(10, int_dy) * base[major_base];
1924 label_dy = major_dy;
1926 /* number of significant digits */
1930 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1935 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1937 n_sig1 = Math.max(n_sig1, n_sig2);
1939 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1940 if (Math.abs(ymin) < 100000)
1941 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) + 1);
1942 if (Math.abs(ymax) < 100000)
1943 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) + 1);
1945 /* increase label_dy if labels would overlap */
1946 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
1948 label_dy = Math.pow(10, int_dy) * base[label_base];
1949 if (label_base % 3 === 2 && major_base % 3 === 1) {
1951 major_dy = Math.pow(10, int_dy) * base[major_base];
1963 y_act = Math.floor(ymin / dy) * dy;
1965 let last_label_y = y1;
1968 ctx.drawLine(x1, y1, x1, y1 - height);
1972 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) / (Math.log(ymax) - Math.log(ymin)) * height;
1974 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
1975 ys = Math.floor(y_screen + 0.5);
1977 if (y_screen < y1 - height - 0.001)
1980 if (y_screen <= y1 + 0.001) {
1981 if (Math.abs(Math.floor(y_act / major_dy + 0.5) - y_act / major_dy) <
1982 dy / major_dy / 10.0) {
1984 if (Math.abs(Math.floor(y_act / label_dy + 0.5) - y_act / label_dy) <
1985 dy / label_dy / 10.0) {
1986 /* label tick mark */
1987 ctx.drawLine(x1, ys, x1 + text, ys);
1990 if (grid != 0 && ys < y1 && ys > y1 - height)
1991 ctx.drawLine(x1, ys, x1 + grid, ys);
1995 str = y_act.toPrecision(n_sig1).stripZeros();
1997 ctx.fillStyle = "black";
1998 if (ys - textHeight / 2 > y1 - height &&
1999 ys + textHeight / 2 < y1)
2000 ctx.fillText(str, x1 + label, ys);
2002 last_label_y = ys - textHeight / 2;
2005 /* major tick mark */
2006 ctx.drawLine(x1, ys, x1 + major, ys);
2009 if (grid != 0 && ys < y1 && ys > y1 - height)
2010 ctx.drawLine(x1, ys, x1 + grid, ys);
2020 /* minor tick mark */
2021 ctx.drawLine(x1, ys, x1 + minor, ys);
2023 /* for logaxis, also put labels on minor tick marks */
2026 str = y_act.toPrecision(n_sig1).stripZeros();
2028 ctx.fillStyle = "black";
2029 if (ys - textHeight / 2 > y1 - height &&
2030 ys + textHeight / 2 < y1 &&
2031 ys + textHeight < last_label_y + 2)
2032 ctx.fillText(str, x1 + label, ys);
2042 /* supress 1.23E-17 ... */
2043 if (Math.abs(y_act) < dy / 100)
2049function mhttpd_resize_sidenav() {
2050 //console.log("mhttpd_resize_sidenav!");
2051 let h = document.getElementById('mheader');
2052 let s = document.getElementById('msidenav');
2053 let top = h.clientHeight + 1 + "px";
2054 if (s.style.top !== top) {
2055 //console.log("httpd_resize_sidenav: top changed from " + s.style.top + " to " + top);
2058 let m = document.getElementById('mmain');
2059 let paddingTop = h.clientHeight + 1 + "px";
2060 if (m.style.paddingTop !== paddingTop) {
2061 //console.log("httpd_resize_sidenav: paddingTop changed from " + m.style.paddingTop + " to " + paddingTop);
2062 m.style.paddingTop = paddingTop;
2066function mhttpd_resize_header() {
2070let mhttpd_last_alarm = 0;
2072function get_options_path(odb_path) {
2073 // Convert "/Path/to/something" to "/Path/to/Options something"
2074 let bits = odb_path.split("/");
2075 bits[bits.length - 1] = "Options " + bits[bits.length - 1];
2076 return bits.join("/");
2079function evalPath(path) {
2081 // if path is a function, evaluate it
2082 if (p && !p.startsWith('/') && p.includes('(') && p.includes(')'))
2087function parseColorSpec(str) {
2088 if (typeof str !== "string")
2092 if (!str.trim().startsWith('['))
2095 // convert string to valid JSON
2097 .replace(/'/g, '"') // 'red' -> "red"
2098 .replace(/\{([^}]+)}/g, '[$1]'); // {0,"red"} → [0,"red"]
2100 const pairs = JSON.parse(str);
2102 str = pairs.map(([value, color]) => ({ value, color }));
2107function mhttpd_refresh(boost) {
2108 if (!mhttpd_initialized)
2111 if (mhttpd_refresh_id !== undefined)
2112 window.clearTimeout(mhttpd_refresh_id);
2114 // don't do a refresh if we are paused
2115 if (mhttpd_refresh_paused) {
2116 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2122 mhttpd_refresh_boost = Date.now();
2124 // don't update page if document is hidden (minimized, covered tab etc), only play alarms
2125 if (document.hidden) {
2127 // only check every 10 seconds for alarm
2128 if (new Date().getTime() > mhttpd_last_alarm + 10000) {
2130 // request current alarms
2131 let req = mjsonrpc_make_request("get_alarms");
2132 mjsonrpc_send_request([req]).then(function (rpc) {
2134 let alarms = rpc[0].result;
2136 // update alarm display
2137 if (alarms.alarm_system_active) {
2139 for (let a in alarms.alarms)
2140 if (alarms.alarms[a].alarm_sound)
2143 mhttpd_alarm_play();
2146 }).catch(function (error) {
2147 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2148 mhttpd_error('Connection to server broken. Trying to reconnect ');
2149 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2150 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2152 mjsonrpc_error_alert(error);
2156 mhttpd_last_alarm = new Date().getTime();
2159 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, 500);
2163 // go through all modb* tags and refresh them -------------------------------------
2165 let modb = getMElements("modb");
2167 let modbvalue = getMElements("modbvalue");
2168 for (let i = 0; i < modbvalue.length; i++) {
2170 let o = modbvalue[i];
2171 if (parseInt(o.dataset.input)) {
2172 if (o.childNodes[0] === undefined) {
2173 ODBInlineEdit(o, evalPath(o.dataset.odbPath), 0);
2176 if (parseInt(o.dataset.odbEditable)) {
2177 if (o.childNodes[0] === undefined || o.childNodes[0].tagName === undefined) {
2179 // add link and event handler if tag is editable
2180 let link = document.createElement('a');
2181 link.href = "javascript:void(0);";
2182 link.onclick = function () {
2183 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2185 link.onfocus = function () {
2186 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2190 o.appendChild(link);
2194 // remove link if present
2195 if (o.childNodes[0] && o.childNodes[0].tagName === 'A')
2196 o.innerHTML = o.childNodes[0].innerHTML;
2200 modbvalue[i].setValue = function (x, tid) {
2201 if (this.dataset.formula !== undefined)
2202 x = eval(this.dataset.formula);
2203 if (tid === undefined)
2206 this.dataset.tid = tid;
2208 if (this.onchange !== null)
2210 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2212 this.dataset.odbLoaded = "1";
2216 modbvalue[i].sendValueToOdb = function () {
2217 mhttpd_refresh_pause(true);
2218 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2219 mhttpd_refresh_pause(false));
2223 let modbcheckbox = getMElements("modbcheckbox");
2224 for (let i = 0; i < modbcheckbox.length; i++) {
2226 // add event listener if missing
2227 if (!modbcheckbox[i].mEventListener) {
2228 modbcheckbox[i].addEventListener("click", function () {
2229 if (this.dataset.validate !== undefined) {
2230 let flag = eval(this.dataset.validate)(this.checked, this);
2236 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.checked ? 1 : 0).then(rpc =>
2237 mhttpd_refresh(true));
2239 modbcheckbox[i].mEventListener = true;
2243 let modbselect = getMElements("modbselect");
2244 for (let i = 0; i < modbselect.length; i++) {
2245 if (modbselect[i].dataset.odbPath) {
2247 // add event listener if missing
2248 if (!modbselect[i].mEventListener) {
2249 modbselect[i].addEventListener("change", function (flag) {
2251 if (flag !== true) { // flag === true if control changed by ODB update
2252 if (this.dataset.validate !== undefined) {
2253 let flag = eval(this.dataset.validate)(this.value, this);
2259 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(rpc =>
2260 mhttpd_refresh(true));
2263 modbselect[i].mEventListener = true;
2267 modbselect[i].setValue = function (x) {
2270 if (this.onchange !== null)
2274 modbselect[i].sendValueToOdb = function () {
2275 mhttpd_refresh_pause(true);
2276 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2277 mhttpd_refresh_pause(false));
2282 let modbbox = getMElements("modbbox");
2283 for (let i = 0; i < modbbox.length; i++) {
2284 modbbox[i].style.border = "1px solid #808080";
2287 // attach "set" function to all ODB buttons
2288 let modbbutton = getMElements("modbbutton");
2289 for (let i = 0; i < modbbutton.length; i++) {
2290 if (!modbbutton[i].mEventListener) {
2291 modbbutton[i].addEventListener("click", function () {
2293 if (this.dataset.validate !== undefined) {
2294 let flag = eval(this.dataset.validate)(this);
2301 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.dataset.odbValue).then(rpc =>
2302 mhttpd_refresh(true));
2304 modbbutton[i].mEventListener = true;
2308 // replace all horizontal bars with proper <div>'s
2309 let modbhbar = getMElements("modbhbar");
2310 for (let i = 0; i < modbhbar.length; i++) {
2311 if (modbhbar[i].childNodes === undefined || modbhbar[i].childNodes[0] === undefined ||
2312 modbhbar[i].childNodes[0].tagName !== 'DIV') {
2313 modbhbar[i].style.display = "block";
2314 if (modbhbar[i].style.position === "")
2315 modbhbar[i].style.position = "relative";
2316 modbhbar[i].style.border = "1px solid #808080";
2317 let color = modbhbar[i].style.color;
2319 modbhbar[i].innerHTML = "<div style='background-color:" + color + ";" + "color:black;" +
2320 "width:0;height:100%; " +
2321 "position:relative; display:inline-block;border-right:1px solid #808080'> </div>";
2323 modbhbar[i].setValue = function (x, tid) {
2324 if (this.dataset.formula !== undefined)
2325 x = eval(this.dataset.formula);
2326 if (tid === undefined)
2328 let mvalue = mie_to_string(tid, x, this.dataset.format);
2331 let html = mhttpd_escape(" " + mvalue);
2333 if (this.dataset.printValue === "1")
2334 this.children[0].innerHTML = html;
2335 let minValue = parseFloat(this.dataset.minValue);
2336 let maxValue = parseFloat(this.dataset.maxValue);
2337 if (isNaN(minValue))
2339 if (this.dataset.log === "1" &&
2342 if (isNaN(maxValue))
2345 if (this.dataset.log === "1")
2346 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2347 (Math.log(maxValue) - Math.log(minValue)));
2349 percent = Math.round(100 * (x - minValue) /
2350 (maxValue - minValue));
2355 this.children[0].style.width = percent + "%";
2356 if (this.onchange !== null)
2358 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2360 this.dataset.odbLoaded = "1";
2362 this.children[0].style.backgroundColor = this.style.color;
2367 // replace all vertical bars with proper <div>'s
2368 let modbvbar = getMElements("modbvbar");
2369 for (let i = 0; i < modbvbar.length; i++) {
2370 if (modbvbar[i].childNodes === undefined || modbvbar[i].childNodes[0] === undefined ||
2371 modbvbar[i].childNodes[0].tagName !== 'DIV') {
2372 modbvbar[i].style.display = "inline-block";
2373 if (modbvbar[i].style.position === "")
2374 modbvbar[i].style.position = "relative";
2375 modbvbar[i].style.border = "1px solid #808080";
2376 let color = modbvbar[i].style.color;
2377 modbvbar[i].innerHTML = "<div style='background-color:" + color + "; height:0; width:100%; " +
2378 "position:absolute; bottom:0; left:0; display:inline-block; border-top:1px solid #808080'> </div>";
2380 modbvbar[i].setValue = function (x, tid) {
2381 if (this.dataset.formula !== undefined)
2382 x = eval(this.dataset.formula);
2383 if (tid === undefined)
2385 let mvalue = mie_to_string(tid, x, this.dataset.format);
2388 let html = mhttpd_escape(" " + mvalue);
2390 if (this.dataset.printValue === "1")
2391 this.children[0].innerHTML = html;
2392 let minValue = parseFloat(this.dataset.minValue);
2393 let maxValue = parseFloat(this.dataset.maxValue);
2394 if (isNaN(minValue))
2396 if (this.dataset.log === "1" &&
2399 if (isNaN(maxValue))
2402 if (this.dataset.log === "1")
2403 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2404 (Math.log(maxValue) - Math.log(minValue)));
2406 percent = Math.round(100 * (x - minValue) /
2407 (maxValue - minValue));
2412 this.children[0].style.height = percent + "%";
2413 if (this.onchange !== null)
2415 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2417 this.dataset.odbLoaded = "1";
2419 this.children[0].style.backgroundColor = this.style.color;
2424 // replace all thermometers with canvas
2425 let modbthermo = getMElements("modbthermo");
2426 for (let i = 0; i < modbthermo.length; i++) {
2427 if (modbthermo[i].childNodes === undefined || modbthermo[i].childNodes[0] === undefined ||
2428 modbthermo[i].childNodes[0].tagName !== 'CANVAS') {
2429 modbthermo[i].style.display = "inline-block";
2430 if (modbthermo[i].style.position === "")
2431 modbthermo[i].style.position = "relative";
2433 cvs = document.createElement("canvas");
2434 if (modbthermo[i].style.width === "") {
2435 dlgAlert("modbthermo #" + i +": undefined width");
2438 if (modbthermo[i].style.height === "") {
2439 dlgAlert("modbthermo #" + i +": undefined height");
2442 let w = Number.parseInt(modbthermo[i].style.width);
2443 let h = Number.parseInt(modbthermo[i].style.height);
2445 w = Math.floor(w / 4) * 4; // 2 must be dividable by 4
2448 modbthermo[i].appendChild(cvs);
2449 modbthermo[i].draw = mhttpd_thermo_draw;
2451 modbthermo[i].setValue = function (x, tid) {
2452 if (this.dataset.formula !== undefined)
2453 x = eval(this.dataset.formula);
2454 if (tid === undefined)
2457 this.dataset.tid = tid;
2459 if (this.onchange !== null)
2461 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2463 this.dataset.odbLoaded = "1";
2469 modbthermo[i].draw();
2473 // replace all gauges with canvas
2474 let modbgauge = getMElements("modbgauge");
2475 for (let i = 0; i < modbgauge.length; i++) {
2476 if (modbgauge[i].childNodes === undefined || modbgauge[i].childNodes[0] === undefined ||
2477 modbgauge[i].childNodes[0].tagName !== 'CANVAS') {
2478 modbgauge[i].style.display = "inline-block";
2479 if (modbgauge[i].style.position === "")
2480 modbgauge[i].style.position = "relative";
2482 let cvs = document.createElement("canvas");
2483 if (modbgauge[i].style.width === "") {
2484 dlgAlert("modbgauge #" + i +": undefined width");
2487 if (modbgauge[i].style.height === "") {
2488 dlgAlert("modbgauge #" + i +": undefined height");
2492 cvs.width = Number.parseInt(modbgauge[i].style.width);
2493 cvs.height = Number.parseInt(modbgauge[i].style.height);
2494 modbgauge[i].appendChild(cvs);
2495 modbgauge[i].draw = mhttpd_gauge_draw;
2497 modbgauge[i].setValue = function (x, tid) {
2498 if (this.dataset.formula !== undefined)
2499 x = eval(this.dataset.formula);
2500 if (tid === undefined)
2503 this.dataset.tid = tid;
2505 if (this.onchange !== null)
2507 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2509 this.dataset.odbLoaded = "1";
2515 modbgauge[i].draw();
2519 // replace all haxis with canvas
2520 let mhaxis = getMElements("mhaxis");
2521 for (let i = 0; i < mhaxis.length; i++) {
2522 if (mhaxis[i].childNodes === undefined || mhaxis[i].childNodes[0] === undefined ||
2523 mhaxis[i].childNodes[0].tagName !== 'CANVAS') {
2524 mhaxis[i].style.display = "block";
2525 if (mhaxis[i].style.position === "")
2526 mhaxis[i].style.position = "relative";
2528 let cvs = document.createElement("canvas");
2529 if (mhaxis[i].style.width === "") {
2530 dlgAlert("mhaxis #" + i +": undefined width");
2533 if (mhaxis[i].style.height === "") {
2534 dlgAlert("mhaxis #" + i +": undefined height");
2537 cvs.width = Number.parseInt(mhaxis[i].style.width) + 2;
2538 cvs.height = Number.parseInt(mhaxis[i].style.height);
2539 mhaxis[i].appendChild(cvs);
2540 mhaxis[i].draw = mhttpd_haxis_draw;
2545 // replace all vaxis with canvas
2546 let mvaxis = getMElements("mvaxis");
2547 for (let i = 0; i < mvaxis.length; i++) {
2548 if (mvaxis[i].childNodes === undefined || mvaxis[i].childNodes[0] === undefined ||
2549 mvaxis[i].childNodes[0].tagName !== 'CANVAS') {
2550 mvaxis[i].style.display = "inline-block";
2551 if (mvaxis[i].style.position === "")
2552 mvaxis[i].style.position = "relative";
2554 let cvs = document.createElement("canvas");
2555 if (mvaxis[i].style.width === "") {
2556 dlgAlert("mvaxis #" + i +": undefined width");
2559 if (mvaxis[i].style.height === "") {
2560 dlgAlert("mvaxis #" + i +": undefined height");
2563 cvs.width = Number.parseInt(mvaxis[i].style.width);
2564 cvs.height = Number.parseInt(mvaxis[i].style.height) + 2;
2565 mvaxis[i].appendChild(cvs);
2566 mvaxis[i].draw = mhttpd_vaxis_draw;
2571 // replace all mhistory tags with history plots
2572 let mhist = getMElements("mhistory");
2573 for (let i = 0; i < mhist.length; i++) {
2574 if (mhist[i].childNodes === undefined || mhist[i].childNodes[0] === undefined ||
2575 mhist[i].childNodes[0].tagName !== 'IMG') {
2576 let w = mhist[i].style.width;
2581 let h = mhist[i].style.height;
2586 mhist[i].innerHTML = "<img src=\"graph.gif?cmd=oldhistory&group=" +
2587 mhist[i].dataset.group +
2588 "&panel=" + mhist[i].dataset.panel +
2589 "&scale=" + mhist[i].dataset.scale +
2592 "&rnd=" + (new Date().getTime()) +
2597 // request all modb* elements
2599 let modbs = ["modb", "modbvalue", "modbcheckbox", "modbselect", "modbbox", "modbhbar",
2600 "modbvbar", "modbthermo", "modbgauge"];
2602 modbs.forEach((mstr) => {
2603 getMElements(mstr).forEach((m) => {
2604 let p = m.dataset.odbPath;
2607 if (p.includes('[*]'))
2608 p = p.replace('[*]', '');
2612 if (mstr === "modbselect" && m.dataset.autoOptions === "1")
2613 paths.push(get_options_path(evalPath(m.dataset.odbPath)));
2618 // request ODB contents for all variables
2619 let req1 = mjsonrpc_make_request("db_get_values", {"paths": paths});
2621 // request current alarms
2622 let req2 = mjsonrpc_make_request("get_alarms");
2624 // request new messages
2625 let req3 = mjsonrpc_make_request("cm_msg_retrieve", {
2626 "facility": "midas",
2631 // request new char messages
2632 let req4 = mjsonrpc_make_request("cm_msg_retrieve", {
2638 mjsonrpc_send_request([req1, req2, req3, req4]).then(function (rpc) {
2640 // update time in header
2641 let dstr = mhttpd_get_display_time().string;
2643 if (document.getElementById("mheader_last_updated") !== undefined) {
2644 //mhttpd_set_innerHTML(document.getElementById("mheader_last_updated"), dstr);
2645 mhttpd_set_firstChild_data(document.getElementById("mheader_last_updated"), dstr);
2650 for (let i = 0; i < modb.length; i++, idata++) {
2651 if (modb[i].dataset.odbPath === undefined) {
2655 let x = rpc[0].result.data[idata];
2656 if (typeof x === 'object' && x !== null) {
2658 if (modb[i].json_value === undefined) {
2659 modb[i].json_value = JSON.stringify(x);
2662 if (modb[i].onchange !== null && JSON.stringify(x) !== modb[i].json_value) {
2663 modb[i].json_value = JSON.stringify(x);
2667 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2669 modb[i].dataset.odbLoaded = "1";
2673 if (modb[i].value === undefined)
2675 if (modb[i].onchange !== null && x !== modb[i].value) {
2679 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2681 modb[i].dataset.odbLoaded = "1";
2686 for (let i = 0; i < modbvalue.length; i++, idata++) {
2687 if (modbvalue[i].dataset.odbPath === undefined) {
2692 if (rpc[0].result.status[idata] === 312) {
2693 modbvalue[i].innerHTML = "ODB key \"" +
2694 evalPath(modbvalue[i].dataset.odbPath) + "\" not found";
2696 let x = rpc[0].result.data[idata];
2697 let tid = rpc[0].result.tid[idata];
2699 modbvalue[i].tid = tid;
2700 if (modbvalue[i].dataset.formula !== undefined)
2701 x = eval(modbvalue[i].dataset.formula);
2702 let mvalue = mie_to_string(tid, x, modbvalue[i].dataset.format);
2705 else if (typeof mvalue === 'string' && mvalue.trim() === "")
2706 mvalue = "(spaces)";
2708 if (tid === TID_BOOL)
2711 html = mhttpd_escape(mvalue);
2712 if (parseInt(modbvalue[i].dataset.odbEditable) || parseInt(modbvalue[i].dataset.input)) {
2713 if (modbvalue[i].childNodes[1] !== undefined &&
2714 modbvalue[i].childNodes[1].innerHTML !== undefined) {
2715 modbvalue[i].childNodes[1].innerHTML = html;
2717 if (modbvalue[i].childNodes[0]) // dataset.input could not yet be initialized (AJAX)
2718 modbvalue[i].childNodes[0].innerHTML = html;
2721 modbvalue[i].innerHTML = html;
2723 if (modbvalue[i].value !== x && modbvalue[i].onchange !== null) {
2724 modbvalue[i].value = x;
2725 if (modbvalue[i].value !== undefined)
2726 modbvalue[i].onchange();
2729 modbvalue[i].value = x;
2731 if (modbvalue[i].dataset.odbLoaded === undefined && modbvalue[i].onload !== null) {
2732 modbvalue[i].onload();
2733 modbvalue[i].dataset.odbLoaded = "1";
2738 for (let i = 0; i < modbcheckbox.length; i++, idata++) {
2739 if (modbcheckbox[i].dataset.odbPath === undefined) {
2744 let x = rpc[0].result.data[idata];
2745 let tid = rpc[0].result.tid[idata];
2746 let mvalue = mie_to_string(tid, x);
2747 if (typeof x === "boolean")
2748 modbcheckbox[i].checked = x;
2750 modbcheckbox[i].checked = (mvalue !== "0");
2751 if (modbcheckbox[i].onchange !== null)
2752 modbcheckbox[i].onchange();
2753 if (modbcheckbox[i].dataset.odbLoaded === undefined && modbcheckbox[i].onload !== null) {
2754 modbcheckbox[i].onload();
2755 modbcheckbox[i].dataset.odbLoaded = "1";
2759 for (let i = 0; i < modbselect.length; i++, idata++) {
2760 if (modbselect[i].dataset.odbPath === undefined) {
2765 let x = rpc[0].result.data[idata];
2766 let auto_options = [];
2768 if (modbselect[i].dataset.autoOptions == "1") {
2770 auto_options = rpc[0].result.data[idata];
2772 if (auto_options !== null && modbselect[i].options.length == 0) {
2773 for (let opt = 0; opt < auto_options.length; opt++) {
2774 modbselect[i].add(new Option(auto_options[opt], auto_options[opt]));
2779 if (rpc[0].result.status[idata] === 312) {
2780 modbselect[i].parentElement.innerHTML = "ODB key \"" +
2781 evalPath(modbselect[i].dataset.odbPath) + "\" not found";
2783 if (x !== null && modbselect[i].value !== x.toString()) {
2785 if (modbselect[i].value !== undefined) {
2786 // check if option is available
2788 for (j = 0; j < modbselect[i].options.length; j++)
2789 if (modbselect[i].options[j].value === x.toString())
2792 // if option is available, set it
2793 if (j < modbselect[i].options.length) {
2794 modbselect[i].value = x.toString();
2795 if (modbselect[i].onchange !== null)
2796 modbselect[i].onchange(true);
2798 // if not, set blank
2799 if (modbselect[i].value !== "")
2800 modbselect[i].value = "";
2804 if (Array.isArray(x)) {
2805 // check if all array values are the same
2808 modbselect[i].value = s.values().next().value;
2810 modbselect[i].value = "...";
2813 if (modbselect[i].dataset.odbLoaded === undefined && modbselect[i].onload !== null) {
2814 modbselect[i].onload();
2815 modbselect[i].dataset.odbLoaded = "1";
2820 for (let i = 0; i < modbbox.length; i++, idata++) {
2821 if (modbbox[i].dataset.odbPath === undefined) {
2826 let x = rpc[0].result.data[idata];
2827 if (modbbox[i].dataset.formula !== undefined)
2828 x = eval(modbbox[i].dataset.formula);
2829 if (x > 0 || x === true) {
2830 modbbox[i].style.backgroundColor = modbbox[i].dataset.color;
2832 if (modbbox[i].dataset.backgroundColor !== undefined)
2833 modbbox[i].style.backgroundColor = modbbox[i].dataset.backgroundColor;
2835 modbbox[i].style.backgroundColor = "";
2837 if (modbbox[i].onchange !== null)
2838 modbbox[i].onchange();
2839 if (modbbox[i].dataset.odbLoaded === undefined && modbbox[i].onload !== null) {
2840 modbbox[i].onload();
2841 modbbox[i].dataset.odbLoaded = "1";
2845 for (let i = 0; i < modbhbar.length; i++, idata++) {
2846 if (modbhbar[i].dataset.odbPath === undefined) {
2851 let x = rpc[0].result.data[idata];
2852 let tid = rpc[0].result.tid[idata];
2854 modbhbar[i].setValue(x, tid);
2857 for (let i = 0; i < modbvbar.length; i++, idata++) {
2858 if (modbvbar[i].dataset.odbPath === undefined) {
2863 let x = rpc[0].result.data[idata];
2864 let tid = rpc[0].result.tid[idata];
2866 modbvbar[i].setValue(x, tid);
2869 for (let i = 0; i < modbthermo.length; i++, idata++) {
2870 if (modbthermo[i].dataset.odbPath === undefined) {
2875 let x = rpc[0].result.data[idata];
2876 let tid = rpc[0].result.tid[idata];
2878 modbthermo[i].setValue(x, tid);
2881 for (let i = 0; i < modbgauge.length; i++, idata++) {
2882 if (modbgauge[i].dataset.odbPath === undefined) {
2887 let x = rpc[0].result.data[idata];
2888 let tid = rpc[0].result.tid[idata];
2890 modbgauge[i].setValue(x, tid);
2893 let alarms = rpc[1].result;
2895 // update alarm display
2896 let e = document.getElementById('mheader_alarm');
2897 if (!alarms.alarm_system_active) {
2898 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: Off</a>");
2899 mhttpd_set_className(e, "mgray mbox");
2904 for (let a in alarms.alarms) {
2908 if (alarms.alarms[a].alarm_sound)
2912 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: None</a>");
2913 mhttpd_set_className(e, "mgreen mbox");
2917 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: " + s + "</a>");
2919 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarm: " + s + "</a>");
2920 mhttpd_set_className(e, "mred mbox");
2923 mhttpd_alarm_play();
2929 if (rpc[2].result.messages !== undefined) {
2930 msg = rpc[2].result.messages.split("\n");
2931 if (msg[msg.length - 1] === "")
2932 msg = msg.slice(0, -1);
2936 // update chat messages
2938 if (rpc[3].result.messages !== undefined) {
2939 chat = rpc[3].result.messages.split("\n");
2940 if (chat[chat.length - 1] === "")
2941 chat = chat.slice(0, -1);
2945 mhttpd_message(msg, chat);
2946 mhttpd_resize_sidenav();
2948 if (mhttpd_refresh_interval !== undefined && mhttpd_refresh_interval > 0) {
2949 // use 100ms refresh period in boost mode (500 ms after last write)
2950 if (Date.now() < mhttpd_refresh_boost + 500)
2951 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, 100);
2953 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2956 }).catch(function (error) {
2958 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2959 mhttpd_error('Connection to server broken. Trying to reconnect ');
2960 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2961 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2963 mjsonrpc_error_alert(error);
2968function mhttpd_refresh_history() {
2969 if (mhttpd_refresh_history_id !== undefined)
2970 window.clearTimeout(mhttpd_refresh_history_id);
2972 /* this fuction gets called by mhttpd_init to periodically refresh all history panels */
2974 let mhist = document.getElementsByName("mhistory");
2975 for (let i = 0; i < mhist.length; i++) {
2976 let s = mhist[i].childNodes[0].src;
2978 if (s.lastIndexOf("&rnd=") !== -1) {
2979 s = s.substr(0, s.lastIndexOf('&rnd='));
2980 s += "&rnd=" + new Date().getTime();
2982 mhist[i].childNodes[0].src = s;
2985 if (mhttpd_refresh_history_interval !== undefined && mhttpd_refresh_history_interval > 0)
2986 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2989function mhttpd_reconnect() {
2990 mjsonrpc_db_ls(["/"]).then(function (rpc) {
2991 // on successful connection remove error and schedule refresh
2992 mhttpd_error_clear();
2993 if (mhttpd_refresh_id !== undefined)
2994 window.clearTimeout(mhttpd_refresh_id);
2995 if (mhttpd_refresh_history_id !== undefined)
2996 window.clearTimeout(mhttpd_refresh_history_id);
2997 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2998 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2999 }).catch(function (error) {
3000 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
3004window.addEventListener('resize', mhttpd_resize_message);
3006function mhttpd_resize_message() {
3007 //console.log("mhttpd_resize_message() via resize event listener");
3008 let d = document.getElementById("mheader_message");
3009 if (d.currentMessage !== undefined && d.style.display !== 'none')
3010 mhttpd_fit_message(d.currentMessage);
3013function mhttpd_close_message() {
3014 let d = document.getElementById("mheader_message");
3016 // remember time of messages to suppress
3017 mhttpdConfigSet('suppressMessageBefore', d.currentMessageT);
3019 d.style.display = "none";
3020 mhttpd_resize_sidenav();
3023function mhttpd_fit_message(m) {
3024 let d = document.getElementById("mheader_message");
3025 let cross = " <span style=\"cursor: pointer;\" onclick=\"mhttpd_close_message();\">╳</span>";
3026 let link1 = "<span style=\"cursor: pointer;\" onclick=\"window.location.href='?cmd=Messages'\">";
3027 let link2 = "</span>";
3028 d.style.display = "inline-block";
3030 // limit message to fit parent element
3032 let parentWidth = d.parentNode.offsetWidth;
3033 let maxWidth = parentWidth - 30;
3035 // check if the full message fits
3037 d.innerHTML = link1 + m + link2 + cross;
3038 //console.log("mhttpd_fit_message: len: " + d.offsetWidth + ", max: " + maxWidth + ", message: " + m);
3039 if (d.offsetWidth <= maxWidth) {
3043 // check if the message minus timestamp and type fits
3045 m = m.substr(m.indexOf(']')+1);
3046 d.innerHTML = link1 + m + link2 + cross;
3047 let w = d.offsetWidth;
3048 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + m);
3049 if (w <= maxWidth) {
3053 // guess the length assuming fix pixels per char
3055 let charWidth = w/m.length;
3056 let guessLength = maxWidth/charWidth - 3; // 3 chars of "..."
3058 let g = m.substr(0, guessLength);
3059 d.innerHTML = link1 + g + "..." + link2 + cross;
3061 //console.log("mhttpd_fit_message: char: " + charWidth + ", guess: " + guessLength + ", len: " + w + ", max: " + maxWidth);
3063 // grow or shrink our guess
3066 //console.log("mhttpd_fit_message: too short, grow");
3067 for (let i=guessLength+1; i<=m.length; i++) {
3068 let s = m.substr(0, i);
3069 d.innerHTML = link1 + s + "..." + link2 + cross;
3071 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + s);
3076 //console.log("mhttpd_fit_message: too long, shrink");
3077 while (g.length > 0) {
3078 g = g.substr(0, g.length-1);
3079 d.innerHTML = link1 + g + "..." + link2 + cross;
3081 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + g);
3088function mhttpd_message(msg, chat) {
3103 if (msg !== undefined) {
3104 lastMsg = msg[0].substr(msg[0].indexOf(" ") + 1);
3105 lastMsgT = parseInt(msg[0]);
3108 if (chat !== undefined) {
3109 lastChat = chat[0].substr(chat[0].indexOf(" ") + 1);
3110 lastChatT = parseInt(chat[0]);
3111 if (chat[0].length > 0)
3112 mTalk = chat[0].substr(chat[0].indexOf("]") + 2);
3114 chatName = lastChat.substring(lastChat.indexOf("[") + 1, lastChat.indexOf(","));
3115 lastChat = lastChat.substr(0, lastChat.indexOf("[")) +
3116 "<b>" + chatName + ":</b>" +
3117 lastChat.substr(lastChat.indexOf("]") + 1);
3120 if (lastChatT > lastMsgT) {
3124 talkTime = lastChatT;
3125 notifyTime = lastChatT;
3129 c = "var(--myellow)";
3130 mTalk = lastMsg.substr(lastMsg.indexOf("]") + 1);
3131 mType = m.substring(m.indexOf(",") + 1, m.indexOf("]"));
3132 talkTime = lastMsgT;
3133 notifyTime = lastMsgT;
3138 let d = document.getElementById("mheader_message");
3139 if (d !== undefined && d !== null && d.currentMessage !== m &&
3140 (mhttpdConfig().suppressMessageBefore === undefined || lastT > mhttpdConfig().suppressMessageBefore)) {
3142 d.style.removeProperty("-webkit-transition");
3143 d.style.removeProperty("transition");
3145 if (mType === "USER" && mhttpdConfig().displayChat ||
3146 mType === "TALK" && mhttpdConfig().displayTalk ||
3147 mType === "ERROR" && mhttpdConfig().displayError ||
3148 mType === "INFO" && mhttpdConfig().displayInfo ||
3149 mType === "LOG" && mhttpdConfig().displayLog) {
3151 let first = (d.currentMessage === undefined);
3152 d.currentMessage = m; // store full message in user-defined attribute
3153 d.currentMessageT = lastMsgT; // store message time in user-defined attribute
3155 mhttpd_fit_message(m);
3156 d.age = new Date() / 1000;
3159 if (m.search("ERROR]") > 0) {
3160 d.style.backgroundColor = "var(--mred)";
3161 d.style.color = "white";
3165 // manage backgroud color (red for errors, fading yellow for others)
3166 if (m.search("ERROR]") > 0) {
3167 d.style.removeProperty("-webkit-transition");
3168 d.style.removeProperty("transition");
3169 d.style.backgroundColor = "var(--mred)";
3171 d.age = new Date() / 1000;
3172 d.style.removeProperty("-webkit-transition");
3173 d.style.removeProperty("transition");
3174 d.style.backgroundColor = c;
3180 if (mType === "USER" && mhttpdConfig().speakChat) {
3181 // do not speak own message
3182 if (document.getElementById("chatName") === undefined ||
3183 document.getElementById("chatName") === null ||
3184 document.getElementById("chatName").value !== chatName) {
3185 mhttpd_speak(talkTime, mTalk);
3187 } else if (mType === "TALK" && mhttpdConfig().speakTalk) {
3188 mhttpd_speak(talkTime, mTalk);
3189 } else if (mType === "ERROR" && mhttpdConfig().speakError) {
3190 mhttpd_speak(talkTime, mTalk);
3191 } else if (mType === "INFO" && mhttpdConfig().speakInfo) {
3192 mhttpd_speak(talkTime, mTalk);
3193 } else if (mType === "LOG" && mhttpdConfig().speakLog) {
3194 mhttpd_speak(talkTime, mTalk);
3198 if (mType === "USER" && mhttpdConfig().notifyChat ||
3199 mType === "TALK" && mhttpdConfig().notifyTalk ||
3200 mType === "ERROR" && mhttpdConfig().notifyError ||
3201 mType === "INFO" && mhttpdConfig().notifyInfo ||
3202 mType === "LOG" && mhttpdConfig().notifyLog) {
3204 // avoid double notification if page gets reloaded
3205 // console.log("notifyTime: " + notifyTime + " last: " + mhttpdConfig().var.lastNotify);
3206 if (mhttpdConfig().var.lastNotify === undefined || notifyTime > mhttpdConfig().var.lastNotify) {
3207 mhttpdConfigSet("var.lastNotify", notifyTime);
3209 if (("Notification" in window)) {
3210 if (Notification.permission === "granted") {
3211 const n = new Notification("MIDAS", {
3213 icon: "./apple-touch-icon.png"
3215 } else if (Notification.permission !== "denied") {
3216 Notification.requestPermission().then((p) => {
3217 if (p === "granted") {
3218 const n = new Notification("MIDAS", {
3220 icon: "./apple-touch-icon.png"
3231 let t = new Date() / 1000;
3232 if (t > d.age + 5 && d.style.backgroundColor === "var(--myellow)") {
3233 let backgroundColor = "var(--mgray)";
3234 d.style.setProperty("-webkit-transition", "background-color 3s", "");
3235 d.style.setProperty("transition", "background-color 3s", "");
3236 d.style.backgroundColor = backgroundColor;
3241function mhttpd_error(error) {
3242 let d = document.getElementById("mheader_error");
3243 if (d !== undefined) {
3244 error += "<div style=\"display: inline; float: right; padding-right: 10px; cursor: pointer;\"" +
3245 " onclick=\"document.getElementById("mheader_error").style.zIndex = 0;\">╳</div>";
3246 d.innerHTML = error;
3247 d.style.zIndex = 3; // above header
3251function mhttpd_error_clear() {
3252 if (document.getElementById("mheader_error")) {
3253 document.getElementById("mheader_error").innerHTML = "";
3254 document.getElementById("mheader_error").style.zIndex = 0; // below header
3258function mhttpd_create_page_handle_create(mouseEvent) {
3262 let arraylength = "";
3263 let stringlength = "";
3265 let form = document.getElementsByTagName('form')[0];
3268 path = form.elements['odb'].value;
3269 type = form.elements['type'].value;
3270 name = form.elements['value'].value;
3271 arraylength = form.elements['index'].value;
3272 stringlength = form.elements['strlen'].value;
3274 let e = document.getElementById("odbpath");
3275 path = JSON.parse(e.innerHTML);
3276 if (path === "/") path = "";
3278 type = document.getElementById("create_tid").value;
3279 name = document.getElementById("create_name").value;
3280 arraylength = document.getElementById("create_array_length").value;
3281 stringlength = document.getElementById("create_strlen").value;
3283 //alert("Path: " + path + " Name: " + name);
3286 if (path === "/") path = "";
3288 if (name.length < 1) {
3289 dlgAlert("Name is too short");
3293 let int_array_length = parseInt(arraylength);
3295 //alert("int_array_length: " + int_array_length);
3297 if (!int_array_length || int_array_length < 1) {
3298 dlgAlert("Bad array length: " + arraylength);
3302 let int_string_length = parseInt(stringlength);
3304 if (!int_string_length || int_string_length < 1) {
3305 dlgAlert("Bad string length " + stringlength);
3310 param.path = path + "/" + name;
3311 param.type = parseInt(type);
3312 if (int_array_length > 1)
3313 param.array_length = int_array_length;
3314 if (int_string_length > 0)
3315 param.string_length = int_string_length;
3317 mjsonrpc_db_create([param]).then(function (rpc) {
3318 let status = rpc.result.status[0];
3319 if (status === 311) {
3320 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3321 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3323 } else if (status !== 1) {
3324 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages.", true, true, function () {
3325 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3328 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3330 }).catch(function (error) {
3331 mjsonrpc_error_alert(error);
3332 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3338function mhttpd_create_page_handle_cancel(mouseEvent) {
3339 dlgHide('dlgCreate');
3343function mhttpd_link_page_handle_link(mouseEvent) {
3344 let e = document.getElementById("link_odbpath");
3345 let path = JSON.parse(e.innerHTML);
3346 if (path === "/") path = "";
3347 //let path = document.getElementById("odb_path").value;
3348 let name = document.getElementById("link_name").value;
3349 let target = document.getElementById("link_target").value;
3351 //console.log("Path: " + path + " Name: " + name + " Target: [" + target + "]");
3353 if (name.length < 1) {
3354 dlgAlert("Name is too short");
3358 if (target.length <= 1) {
3359 dlgAlert("Link target is too short");
3364 param.new_links = [path + "/" + name];
3365 param.target_paths = [target];
3367 mjsonrpc_call("db_link", param).then(function (rpc) {
3368 let status = rpc.result.status[0];
3369 if (status === 304) {
3370 dlgMessage("Error", "Invalid link, see MIDAS messages.", true, true, function () {
3371 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3373 } else if (status === 311) {
3374 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3375 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3377 } else if (status === 312) {
3378 dlgMessage("Error", "Target path " + target + " does not exist in ODB.", true, true, function () {
3379 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3381 } else if (status === 315) {
3382 dlgMessage("Error", "ODB data type mismatch, see MIDAS messages.", true, true, function () {
3383 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3385 } else if (status !== 1) {
3386 dlgMessage("Error", "db_create_link() error " + status + ", see MIDAS messages.", true, true, function () {
3387 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3390 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3392 }).catch(function (error) {
3393 mjsonrpc_error_alert(error);
3394 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3400function mhttpd_link_page_handle_cancel(mouseEvent) {
3405function mhttpd_delete_page_handle_delete(mouseEvent, xpath) {
3406 let form = document.getElementsByTagName('form')[0];
3411 path = form.elements['odb'].value;
3413 if (path === "/") path = "";
3415 for (let i = 0; ; i++) {
3417 let v = form.elements[n];
3418 if (v === undefined)
3421 names.push(path + "/" + v.value);
3424 let e = document.getElementById("odbpath");
3425 path = JSON.parse(e.innerHTML);
3426 if (path === "/") path = "";
3428 //alert("Path: " + path);
3430 for (i = 0; ; i++) {
3431 let v = document.getElementById("delete" + i);
3432 if (v === undefined || v === null)
3435 let name = JSON.parse(v.value);
3436 if (name.length > 0) {
3437 names.push(path + "/" + name);
3442 //alert("Names: " + names);
3446 if (names.length < 1) {
3447 dlgAlert("Please select at least one ODB entry to delete.");
3454 params.paths = names;
3455 mjsonrpc_call("db_delete", params).then(function (rpc) {
3457 let status = rpc.result.status;
3458 //alert(JSON.stringify(status));
3459 for (let i = 0; i < status.length; i++) {
3460 if (status[i] !== 1) {
3461 message += "Cannot delete \"" + rpc.request.params.paths[i] + "\", db_delete_key() status " + status[i] + "\n";
3464 if (message.length > 0) {
3467 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3469 }).catch(function (error) {
3470 mjsonrpc_error_alert(error);
3471 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3477function mhttpd_delete_page_handle_cancel(mouseEvent) {
3478 dlgHide('dlgDelete');
3482function mhttpd_start_run(ret) {
3483 mhttpd_goto_page("Start", ret); // DOES NOT RETURN
3486function mhttpd_stop_run(ret) {
3487 dlgConfirm('Are you sure to stop the run?', function (flag) {
3488 if (flag === true) {
3489 mjsonrpc_call("cm_transition", {"transition": "TR_STOP"}).then(function (rpc) {
3490 //mjsonrpc_debug_alert(rpc);
3491 if (rpc.result.status !== 1) {
3492 throw new Error("Cannot stop run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3494 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3495 }).catch(function (error) {
3496 mjsonrpc_error_alert(error);
3503function mhttpd_pause_run(ret) {
3504 dlgConfirm('Are you sure to pause the run?', function (flag) {
3505 if (flag === true) {
3506 mjsonrpc_call("cm_transition", {"transition": "TR_PAUSE"}).then(function (rpc) {
3507 //mjsonrpc_debug_alert(rpc);
3508 if (rpc.result.status !== 1) {
3509 throw new Error("Cannot pause run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3511 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3512 }).catch(function (error) {
3513 mjsonrpc_error_alert(error);
3520function mhttpd_resume_run(ret) {
3521 dlgConfirm('Are you sure to resume the run?', function (flag) {
3522 if (flag === true) {
3523 mjsonrpc_call("cm_transition", {"transition": "TR_RESUME"}).then(function (rpc) {
3524 //mjsonrpc_debug_alert(rpc);
3525 if (rpc.result.status !== 1) {
3526 throw new Error("Cannot resume run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3528 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3529 }).catch(function (error) {
3530 mjsonrpc_error_alert(error);
3536function mhttpd_cancel_transition() {
3537 dlgConfirm('Are you sure to cancel the currently active run transition?', function (flag) {
3538 if (flag === true) {
3542 paths.push("/Runinfo/Requested Transition");
3544 paths.push("/Runinfo/Transition in progress");
3548 params.paths = paths;
3549 params.values = values;
3551 mjsonrpc_call("db_paste", params).then(function (rpc) {
3552 //mjsonrpc_debug_alert(rpc);
3553 if ((rpc.result.status[0] !== 1) || (rpc.result.status[1] !== 1)) {
3554 throw new Error("Cannot cancel transition, db_paste() status " + rpc.result.status + ", see MIDAS messages");
3556 mhttpd_goto_page("Transition"); // DOES NOT RETURN
3557 }).catch(function (error) {
3558 mjsonrpc_error_alert(error);
3564function mhttpd_reset_alarm(alarm_name) {
3565 mhttpd_alarm_snooze_clear();
3566 mjsonrpc_call("al_reset_alarm", {"alarms": [alarm_name]}).then(function (rpc) {
3567 //mjsonrpc_debug_alert(rpc);
3568 if (rpc.result.status[0] !== 1 && rpc.result.status[0] !== 1004) {
3569 throw new Error("Cannot reset alarm, status " + rpc.result.status + ", see MIDAS messages");
3571 }).catch(function (error) {
3572 mjsonrpc_error_alert(error);
3576/*---- site and session storage ----------------------------*/
3581 flag = mhttpdConfig().speakChat; // read
3583 mhttpdConfigSet('speakChat', false); // write individual config
3585 let c = mhttpdConfig(); // write whole config
3586 c.speakChat = false;
3588 mhttpdConfigSetAll(c);
3591 Saves settings are kept in local storage, which gets
3592 cleared when the browser session ends. Then the default
3593 values are returned.
3596let mhttpd_config_defaults = {
3604 'displayChat': true,
3605 'displayTalk': true,
3606 'displayError': true,
3607 'displayInfo': false,
3608 'displayLog': false,
3612 'speakError': false,
3616 'displayChat': true,
3617 'displayTalk': true,
3618 'displayError': true,
3619 'displayInfo': true,
3620 'displayLog': false,
3622 'speakVoice': 'Alex',
3626 'alarmSoundFile': 'beep.mp3',
3630 'timezone': 'local',
3637 'suppressMessageBefore': 0,
3640 'facility': 'midas',
3645function mhttpdConfigODB(callback) {
3646 let c = mhttpd_config_defaults;
3648 if (localStorage.mhttpd) {
3649 c = JSON.parse(localStorage.mhttpd);
3652 // obtain some defaults from ODB
3653 mjsonrpc_db_get_value("/Experiment/Enable sound").then(function (rpc) {
3654 let flag = rpc.result.data[0];
3655 if (flag === null) {
3656 mjsonrpc_db_create([{"path" : "/Experiment/Enable sound", "type" : TID_BOOL}]).then(function (rpc) {
3657 mjsonrpc_db_paste(["/Experiment/Enable sound"],[true]).then(function(rpc) {}).catch(function(error) {
3658 mjsonrpc_error_alert(error); });
3659 }).catch(function (error) {
3660 mjsonrpc_error_alert(error);
3662 } else if (flag === false) {
3663 c.speakChat = false;
3664 c.speakTalk = false;
3665 c.speakError = false;
3666 c.speakInfo = false;
3668 c.alarmSound = false;
3669 localStorage.setItem('mhttpd', JSON.stringify(c));
3672 }).catch(function (error) {
3673 mjsonrpc_error_alert(error);
3680function mhttpdConfig() {
3681 let c = mhttpd_config_defaults;
3683 if (localStorage.mhttpd)
3684 c = JSON.parse(localStorage.mhttpd);
3686 // if element has been added to mhttpd_config_defaults, merge it
3687 if (Object.keys(c).length !== Object.keys(mhttpd_config_defaults).length) {
3688 for (let o in mhttpd_config_defaults)
3690 c[o] = mhttpd_config_defaults[o];
3698function mhttpdConfigSet(item, value) {
3700 let c = mhttpdConfig();
3701 if (item.indexOf('.') > 0) {
3702 let c1 = item.substring(0, item.indexOf('.'));
3703 let c2 = item.substring(item.indexOf('.') + 1);
3707 localStorage.setItem('mhttpd', JSON.stringify(c));
3712function mhttpdConfigSetAll(new_config) {
3714 localStorage.setItem('mhttpd', JSON.stringify(new_config));
3719/*---- sound and speak functions --------------------------*/
3721let last_audio = null;
3722let inside_new_audio = false;
3724let mhttpd_alarm_play_pending = false;
3726const mhttpd_alarm_snooze_storage_key = "mhttpdAlarmSnoozeUntil";
3728function mhttpd_alarm_snooze_remaining() {
3729 let snooze_until = 0;
3732 snooze_until = parseInt(sessionStorage.getItem(mhttpd_alarm_snooze_storage_key), 10);
3737 if (!Number.isFinite(snooze_until)) {
3738 sessionStorage.removeItem(mhttpd_alarm_snooze_storage_key);
3742 let remaining = Math.ceil((snooze_until - Date.now()) / 1000);
3743 if (remaining <= 0) {
3744 sessionStorage.removeItem(mhttpd_alarm_snooze_storage_key);
3751function mhttpd_alarm_is_snoozed() {
3752 return mhttpd_alarm_snooze_remaining() > 0;
3755function mhttpd_alarm_snooze(seconds) {
3756 let snooze_until = Date.now() + seconds * 1000;
3759 sessionStorage.setItem(mhttpd_alarm_snooze_storage_key, snooze_until.toString());
3762 mhttpd_alarm_play_pending = false;
3764 // Stop alarm output that was already in progress when Snooze was selected.
3767 last_audio.src = "";
3770 if ('speechSynthesis' in window)
3771 speechSynthesis.cancel();
3773 mhttpd_alarm_snooze_update_buttons();
3776function mhttpd_alarm_snooze_clear() {
3778 sessionStorage.removeItem(mhttpd_alarm_snooze_storage_key);
3782 // A reset alarm must not suppress the sound of the next alarm through the
3783 // repeat timer left behind by the previous one.
3784 mhttpdConfigSet("var.lastAlarm", 0);
3785 mhttpd_alarm_play_pending = false;
3787 mhttpd_alarm_snooze_close_menus();
3788 mhttpd_alarm_snooze_update_buttons();
3791function mhttpd_alarm_snooze_close_menus() {
3792 let menus = document.getElementsByClassName("alarmSnoozeMenu");
3793 for (let i = 0; i < menus.length; i++)
3794 menus[i].style.display = "none";
3797function mhttpd_alarm_snooze_toggle_menu(event) {
3798 if (event.type === "keydown") {
3799 if (event.key !== "Enter" && event.key !== " " || event.repeat)
3803 event.preventDefault();
3804 event.stopPropagation();
3805 let menu = event.currentTarget.nextElementSibling;
3806 let show_menu = menu.style.display !== "block";
3808 mhttpd_alarm_snooze_close_menus();
3809 menu.style.display = show_menu ? "block" : "none";
3812function mhttpd_alarm_snooze_select(event, seconds) {
3813 event.stopPropagation();
3814 mhttpd_alarm_snooze(seconds);
3815 mhttpd_alarm_snooze_close_menus();
3818function mhttpd_alarm_snooze_update_buttons() {
3819 let remaining = mhttpd_alarm_snooze_remaining();
3820 let label = "Snooze...";
3822 if (remaining > 0) {
3823 let minutes = Math.floor(remaining / 60);
3824 let seconds = remaining % 60;
3825 label = "Snooze " + minutes.toString().padStart(2, "0") + ":" +
3826 seconds.toString().padStart(2, "0");
3829 let buttons = document.getElementsByClassName("mAlarmSnoozeButton");
3830 for (let i = 0; i < buttons.length; i++)
3831 buttons[i].textContent = label;
3834document.addEventListener("click", mhttpd_alarm_snooze_close_menus);
3837// Do not delete the following code. It is not used at this moment,
3838// but may become necessary in the future. Included
3839// is an alternative method of allocating and releasing Audio objects
3840// and code for tracing Audio object life time and activity. It is needed
3841// to debug interactions between Audio objects and throttled javascript code
3842// in inactive tabs; and with the "user did not interact with this tab" business.
3845//function mhttpd_alarm_done() {
3848// ended = last_audio.ended;
3849// last_audio = null;
3851// count_audio_done++;
3852// console.log(Date() + ": mhttpd_alarm_done: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended);
3855//function mhttpd_audio_loadeddata(e) {
3856// console.log(Date() + ": mhttpd_audio_loadeddata: counter " + e.target.counter);
3859//function mhttpd_audio_canplay(e) {
3860// console.log(Date() + ": mhttpd_audio_canplay: counter " + e.target.counter);
3863//function mhttpd_audio_canplaythrough(e) {
3864// console.log(Date() + ": mhttpd_audio_canplaythrough: counter " + e.target.counter);
3867//function mhttpd_audio_ended(e) {
3868// console.log(Date() + ": mhttpd_audio_ended: counter " + e.target.counter);
3871//function mhttpd_audio_paused(e) {
3872// console.log(Date() + ": mhttpd_audio_paused: counter " + e.target.counter);
3875function mhttpd_alarm_play_now() {
3876 if (mhttpd_alarm_is_snoozed())
3882 // check for playing the alarm sound is done every few minutes.
3883 // it takes 3 seconds to play the alarm sound
3884 // so in theory, this check is not needed: previous alarm sound should
3885 // always have finished by the time we play a new sound.
3887 // However, observed with google-chrome, in inactive and/or iconized tabs
3888 // javascript code is "throttled" and the above time sequence is not necessarily
3891 // Specifically, I see the following sequence: after 1-2 days of inactivity,
3892 // (i.e.) the "programs" page starts consuming 10-15% CPU, if I open it,
3893 // CPU use goes to 100% for a short while, memory use goes from ~50 Mbytes
3894 // to ~150 Mbytes. console.log() debug print out shows that there are ~400
3895 // accumulated Audio() objects that have been created but did not finish playing
3896 // (because google-chrome is throttling javascript in inactive tabs?), and now
3897 // they all try to load the mp3 file and play all at the same time.
3899 // This fix for this observed behaviour is to only create new Audio() objects
3900 // if previous one finished playing.
3904 if (!last_audio.ended) {
3905 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: previous alarm sound did not finish playing yet");
3910 if (inside_new_audio) {
3911 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: already inside \"new Audio()\"");
3914 inside_new_audio = true;
3916 // reference counting of Audio objects. Not in use today. Do not remove. K.O. Jan 2021
3917 //console.log(Date() + ": mhttpd_alarm_play: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended + ", audio.play!");
3918 //count_audio_created++;
3920 let audio = new Audio(mhttpdConfig().alarmSoundFile);
3921 audio.volume = mhttpdConfig().alarmVolume;
3922 audio.counter = ++count_audio;
3925 inside_new_audio = false;
3927 // code for tracing activity of Audio objects. do not remove. K.O. Jan 2021
3928 //audio.addEventListener("loadeddata", mhttpd_audio_loadeddata);
3929 //audio.addEventListener("canplay", mhttpd_audio_canplay);
3930 //audio.addEventListener("canplaythrough", mhttpd_audio_canplaythrough);
3931 //audio.addEventListener("ended", mhttpd_audio_ended);
3932 //audio.addEventListener("paused", mhttpd_audio_paused);
3934 let promise = audio.play();
3936 promise.then(function(e) {
3937 mhttpd_alarm_play_pending = false;
3938 //console.log(Date() + ": mhttpd_alarm_play: promise fulfilled, counter " + audio.counter);
3939 }).catch(function(e) {
3940 //count_audio_done++;
3941 //console.log(Date() + ": mhttpd_alarm_play: audio.play() exception: " + e + ", created: " + count_audio_created + ", done: " + count_audio_done);
3942 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: audio.play() exception: " + e + ", counter " + audio.counter);
3943 // NB: must clear the URL of the sound file, otherwise, the sound file is still loaded (but not played)
3944 // the loading of sound files is observed to be delayed in inactive tabs
3945 // resulting in many (100-1000) pending loads getting queued, observed
3946 // to all of them attempt to run (load the sound file) in parallel,
3947 // consuming memory (100-300 Mbytes) and CPU (10-15% CPU per inactive tab).
3949 // NB: setting audio to pause() does not seem to do anything.
3952 if (e.name === "NotAllowedError") {
3953 // Automatic media playback can be rejected until the document
3954 // receives a user gesture. Do not let the alarm repeat timer hide
3955 // the failed attempt, and retry from the next ordinary click.
3956 mhttpd_alarm_play_pending = true;
3957 mhttpdConfigSet("var.lastAlarm", 0);
3964function mhttpd_alarm_play() {
3965 if (mhttpd_alarm_is_snoozed())
3968 if (mhttpd_alarm_play_pending)
3971 if (mhttpdConfig().alarmSound && mhttpdConfig().alarmSoundFile) {
3972 let now = new Date() / 1000;
3973 let last = mhttpdConfig().var.lastAlarm;
3974 let next = last + parseFloat(mhttpdConfig().alarmRepeat);
3975 let wait = next - now;
3976 let do_play = (now > next);
3977 //console.log("mhttpd_alarm_play: now: " + now + ", next: " + next + ", last: " + last + ", wait: " + wait + ", play: " + do_play);
3979 mhttpdConfigSet("var.lastAlarm", now);
3980 mhttpd_alarm_play_now();
3985function mhttpd_alarm_retry_pending() {
3986 if (!mhttpd_alarm_play_pending || mhttpd_alarm_is_snoozed())
3989 mhttpd_alarm_play_pending = false;
3990 mhttpd_alarm_play();
3993document.addEventListener("click", mhttpd_alarm_retry_pending);
3995function mhttpd_speak_now(text) {
3996 if (mhttpd_alarm_is_snoozed())
3999 let u = new SpeechSynthesisUtterance(text);
4000 u.voice = speechSynthesis.getVoices().filter(function (voice) {
4001 return voice.name === mhttpdConfig().speakVoice;
4003 u.volume = mhttpdConfig().speakVolume;
4004 speechSynthesis.speak(u);
4007function mhttpd_speak(time, text) {
4009 if (!('speechSynthesis' in window))
4012 if (mhttpd_alarm_is_snoozed())
4015 if (mhttpdConfig().speakChat) {
4016 if (time > mhttpdConfig().var.lastSpeak) {
4017 mhttpdConfigSet("var.lastSpeak", time);
4018 mhttpd_speak_now(text);
4027 * js-indent-level: 3
4028 * indent-tabs-mode: nil