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_spinning_wheel;
881let mhttpd_initialized = false;
883function mhttpd_init(current_page, interval, callback) {
885 This function should be called from custom pages to initialize all ODB tags and refresh
886 them periodically every "interval" in ms
888 For possible ODB tags please refer to
890 https://midas.triumf.ca/MidasWiki/index.php/New_Custom_Pages_(2017)
893 if (mhttpd_initialized) {
894 dlgAlert("mhttpd_init() is called more than once from user code");
899 url = mhttpd_getParameterByName("URL");
901 mjsonrpc_set_url(url);
903 // retrieve current page if not given
904 if (current_page === undefined) {
905 current_page = mhttpd_getParameterByName("page");
907 if (current_page === undefined || current_page === null) {
908 dlgAlert("Please specify name of page via mhttpd_init('<name>')<br>or make sure to have '&page=<name>' in URL");
913 let h = document.getElementById("mheader");
915 dlgAlert('Web page does not contain "mheader" element');
918 let s = document.getElementById("msidenav");
920 dlgAlert('Web page does not contain "msidenav" element');
924 mhttpd_initialized = true;
926 // set global font size
927 let config = mhttpdConfig();
928 document.body.style.fontSize = config.fontSize + "pt";
930 h.style.display = "flex";
932 "<div style='display:inline-block; flex:none;'>" +
933 "<span class='mmenuitem' style='padding: 10px;margin-right: 20px;' onclick='mhttpd_toggle_menu()'>☰</span>" +
934 "<span id='mheader_expt_name'></span>" +
937 "<div style='flex:auto;'>" +
938 " <div id='mheader_message'></div>" +
941 "<div style='display: inline; flex:none;'>" +
942 " <div id='mheader_alarm'> </div>" +
943 " <div style='display: inline; font-size: 75%; margin-right: 10px' id='mheader_last_updated'>mheader_last_updated</div>" +
946 mhttpd_resize_sidenav();
947 window.addEventListener('resize', mhttpd_resize_sidenav);
949 // put error header in front of header
950 let d = document.createElement('div');
951 d.id = 'mheader_error';
952 h.parentNode.insertBefore(d, h);
954 // update header and menu
955 if (document.getElementById("msidenav") !== undefined) {
957 // get it from session storage cache if present
958 if (sessionStorage.msidenav !== undefined && sessionStorage.mexpname !== undefined) {
959 let menu = document.getElementById("msidenav");
960 menu.innerHTML = sessionStorage.msidenav;
961 let item = menu.children;
962 for (let i = 0; i < item.length; i++) {
963 if (item[i].className !== "mseparator") {
964 if (current_page.search(item[i].innerHTML) !== -1)
965 item[i].className = "mmenuitem mmenuitemsel";
967 item[i].className = "mmenuitem";
970 document.getElementById("mheader_expt_name").innerHTML = sessionStorage.mexpname;
972 // now the side navigation has its full width, adjust the main body and make it visible
973 let m = document.getElementById("mmain");
974 if (m !== undefined) {
975 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
976 m.style.opacity = "1";
980 // request it from server, since it might have changed
981 mjsonrpc_db_get_values(["/Experiment/Name", "/Experiment/Menu", "/Experiment/Menu Buttons",
982 "/Custom", "/Script", "/Alias"]).then(function (rpc) {
984 let expt_name = rpc.result.data[0];
985 let menu = rpc.result.data[1];
986 let buttons = rpc.result.data[2];
987 let custom = rpc.result.data[3];
988 let script = rpc.result.data[4];
989 let alias = rpc.result.data[5];
991 document.getElementById("mheader_expt_name").innerHTML = expt_name;
992 sessionStorage.setItem("mexpname", expt_name);
994 // preload spinning wheel for later use
995 if (mhttpd_spinning_wheel === undefined) {
996 mhttpd_spinning_wheel = new Image();
997 mhttpd_spinning_wheel.src = "spinning-wheel.gif";
1003 for (let k in menu) {
1004 if (k.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1006 if (menu[k]) // show button if not disabled
1007 b.push(menu[k + "/name"]);
1009 } else if (buttons && buttons.length > 0) {
1010 b = buttons.split(",");
1013 if (!b || b.length < 1) {
1014 b = ["Status", "ODB", "Messages", "Chat", "ELog", "Alarms", "Programs", "History", "MSCB", "Sequencer", "Config", "Help"];
1019 for (let i = 0; i < b.length; i++) {
1020 let bb = b[i].trim();
1021 let cc = "mmenuitem";
1023 if (bb === current_page) {
1024 cc += " mmenuitemsel";
1026 if (bb == "PySequencer") {
1027 cmd = "Sequencer&SeqODB=/PySequencer";
1029 html += "<div class='" + cc + "'><a href='?cmd=" + cmd + "' class='mmenulink'>" + bb + "</a></div>\n";
1033 if (custom !== null && Object.keys(custom).length > 0) {
1035 html += "<div class='mseparator'></div>\n";
1036 // add menu items recursively
1037 html = mhttpd_add_menu_items(html, custom, current_page, "", 0);
1041 if (script !== null && Object.keys(script).length > 0) {
1043 html += "<div class='mseparator'></div>\n";
1045 for (let b in script) {
1046 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1048 let n = script[b + "/name"];
1049 //html += "<div class='mmenuitem'><a href='?script=" + b + "' class='mmenulink'>" + n + "</a></div>\n";
1050 html += "<div class='mmenuitem'><button class='mbutton' onclick='mhttpd_exec_script(\"" + n + "\")'>" + n + "</button></div>\n";
1056 if (alias !== null && Object.keys(alias).length > 0) {
1058 html += "<div class='mseparator'></div>\n";
1061 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1063 n = alias[b + "/name"];
1064 if (n.substr(n.length - 1) === "&") {
1065 n = n.substr(0, n.length - 1);
1066 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink' target='_blank'>" + n + "↗</a></div>\n";
1068 html += "<div class='mmenuitem'><a href='" + alias[b] + "' class='mmenulink'>" + n + "</a></div>\n";
1074 // dummy spacer to fix scrolling to the bottom, must be at least header height
1075 html += "<div style='height: 64px;'></div>\n";
1077 document.getElementById("msidenav").innerHTML = html;
1079 // re-adjust size of mmain element if menu has changed
1080 let m = document.getElementById("mmain");
1081 if (m !== undefined) {
1082 m.style.marginLeft = document.getElementById("msidenav").clientWidth + "px";
1083 m.style.opacity = "1";
1086 // cache navigation buttons in browser local storage
1087 sessionStorage.setItem("msidenav", html);
1089 // show/hide sidenav according to local storage settings
1090 mhttpd_show_menu(mhttpdConfig().showMenu);
1092 // hide all invisible menu items after layout has been finished
1093 let mi = document.getElementsByClassName('mmenuitem');
1095 let p = m.parentElement;
1096 if (p.style.visibility === "hidden") {
1097 p.style.visibility = "visible";
1098 p.style.display = "none";
1101 mi = document.getElementsByClassName('msubmenuitem');
1103 let p = m.parentElement;
1104 if (p.style.visibility === "hidden") {
1105 p.style.visibility = "visible";
1106 p.style.display = "none";
1110 }).then(function () {
1111 if (callback !== undefined)
1113 }).catch(function (error) {
1114 mjsonrpc_error_alert(error);
1118 // store refresh interval and do initial refresh
1119 if (interval === undefined)
1121 mhttpd_refresh_interval = interval;
1122 mhttpd_refresh_paused = false;
1124 // history interval is static for now
1125 mhttpd_refresh_history_interval = 30000;
1127 // trigger first refresh
1129 mhttpd_refresh_history();
1132function mhttpd_find_menuitem(current_page, submenu) {
1133 for (let b in submenu) {
1135 if (typeof submenu[b] === "object") { // recurse into submenu
1136 let flag = mhttpd_find_menuitem(current_page, submenu[b]);
1141 if (typeof submenu[b] === "string") {
1143 if (s[s.length-1] === '&')
1146 if (b.indexOf('/name') !== -1 && s.toLowerCase() === current_page.toLowerCase())
1155function mhttpd_add_menu_items(html, custom, current_page, path, level) {
1156 for (let b in custom) {
1157 if (b.indexOf('/') >= 0) // skip <key>/last_written and <key>/name
1159 if (b === "path" || b === "images" || b === "default") // skip "path", "Images" and "default"
1162 if (typeof custom[b] === "object") { // submenu created by subdirectory in ODB
1163 let cc = "msubmenuitem";
1165 let expand = mhttpd_find_menuitem(current_page, custom[b]);
1167 // add blanks according to nesting level
1168 for (let i=0 ; i<level ; i++)
1169 l += " ";
1172 l += "▾ "; // caret down
1174 l += "▸ "; // caret right
1177 l += custom[b + "/name"];
1183 html += "<div class='" + cc + "' onclick='mhttpd_submenu(this)'><div class='mmenulink'>" + l + "</div></div>\n";
1186 html += "<div>"; // do not hide submenu if current page is under it
1188 html += "<div style='visibility: hidden'>";
1189 html = mhttpd_add_menu_items(html, custom[b], current_page, p, level+1);
1191 } else if (typeof custom[b] === "string") { // skip any items that don't have type of string, since can't be valid links
1192 let cc = "mmenuitem";
1193 let mitem = custom[b + "/name"];
1194 if (mitem.slice(-1) === '&')
1195 mitem = mitem.slice(0, -1);
1196 if (current_page.search(mitem) !== -1 ||
1197 current_page.search(mitem.toLowerCase()) !== -1)
1198 cc += " mmenuitemsel";
1200 for (let i = 0; i < level; i++)
1201 ln += " ";
1202 ln += custom[b + "/name"];
1204 if (custom[b].substring(0, 5) === "link:") {
1205 let lt = custom[b].substring(5);
1206 html += "<div class='" + cc + "'><a href='" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1211 lt += custom[b + "/name"];
1212 if (ln.slice(-1) === '!')
1216 let clicklink = false;
1218 // style = "style='display: none'";
1220 // Accepted file extentions, if not create onclick call from value
1221 const validExtensionsRegex = /\.([a-zA-Z0-9]+)$/; // /\.(txt|jpg|jpeg|png|gif|pdf|csv|html|htm|js|css)$/i;
1223 if (ln.slice(-1) === '&') {
1224 ln = ln.slice(0, -1);
1225 lt = lt.slice(0, -1);
1226 target += "target='_blank' ";
1227 } else if (!validExtensionsRegex.test(custom[b])) {
1232 const handler = custom[b]
1233 .replace(/&/g, "&") // must do ampersand first
1234 .replace(/"/g, """)
1235 .replace(/'/g, "'");
1236 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`;
1238 html += "<div class='" + cc + "' " + style + "><a " + target + " href='?cmd=custom&page=" + lt + "' class='mmenulink'>" + ln + "</a></div>\n";
1248function mhttpd_submenu(o) {
1250 for (i=0 ; o.firstChild.innerHTML.charCodeAt(i) !== 9656 && o.firstChild.innerHTML.charCodeAt(i) !== 9662 ; i++);
1251 if (o.firstChild.innerHTML.charCodeAt(i) === 9656) {
1253 o.firstChild.innerHTML =
1254 o.firstChild.innerHTML.substring(0, i-1) +
1255 String.fromCharCode(9662) +
1256 o.firstChild.innerHTML.substring(i+1);
1257 o.nextElementSibling.style.display = "inline";
1258 o.nextElementSibling.style.visibility = "visible";
1261 o.firstChild.innerHTML =
1262 o.firstChild.innerHTML.substring(0, i-1) +
1263 String.fromCharCode(9656) +
1264 o.firstChild.innerHTML.substring(i+1);
1265 o.nextElementSibling.style.display = "none";
1269function mhttpd_refresh_pause(flag) {
1270 mhttpd_refresh_paused = flag;
1273function mhttpd_set_refresh_interval(interval) {
1274 mhttpd_refresh_interval = interval;
1277function getMElements(name) {
1278 // collect all <div name=[name] >
1280 e.push(...document.getElementsByName(name));
1282 // collect all <div class=[name] >
1283 e.push(...document.getElementsByClassName(name));
1288function mhttpd_thermo_draw() {
1289 let ctx = this.firstChild.getContext("2d");
1291 let w = this.firstChild.width;
1292 let h = this.firstChild.height;
1293 ctx.clearRect(0, 0, w, h);
1294 w = w - 1; // space for full circles
1297 if (this.dataset.scale === "1") {
1299 w = Math.floor(w / 4) * 4;
1302 if (this.dataset.printValue === "1") {
1306 let x0 = Math.round(w / 4 * 0);
1307 let x1 = Math.round(w / 4 * 1);
1308 let x2 = Math.round(w / 4 * 2);
1309 let x3 = Math.round(w / 4 * 3);
1312 if (v < this.dataset.minValue)
1313 v = this.dataset.minValue;
1314 if (v > this.dataset.maxValue)
1315 v = this.dataset.maxValue;
1316 let yt = (h - 4 * x1) - (h - 5 * x1) * (v - this.dataset.minValue) / (this.dataset.maxValue - this.dataset.minValue);
1318 ctx.translate(0.5, 0.5);
1319 ctx.strokeStyle = "#000000";
1320 ctx.fillStyle = "#FFFFFF";
1325 ctx.arc(x2, x1, x1, Math.PI, 0);
1326 ctx.lineTo(x3, h - x1 * 4);
1327 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1328 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1329 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1332 if (this.dataset.backgroundColor !== undefined) {
1333 ctx.fillStyle = this.dataset.backgroundColor;
1338 if (this.dataset.color === undefined) {
1339 ctx.strokeStyle = "#000000";
1340 ctx.fillStyle = "#000000";
1342 ctx.strokeStyle = this.dataset.color;
1343 ctx.fillStyle = this.dataset.color;
1347 ctx.moveTo(x1 + 3, yt);
1348 ctx.lineTo(x3 - 3, yt);
1349 ctx.lineTo(x3 - 3, h - x2);
1350 ctx.lineTo(x1 + 3, h - x2);
1351 ctx.lineTo(x1 + 3, yt);
1356 ctx.arc(x2, h - x2, x2 - 4, 0, 2 * Math.PI);
1360 // re-draw outer "glass"
1361 ctx.strokeStyle = "#000000";
1362 ctx.fillStyle = "#FFFFFF";
1365 ctx.arc(x2, x1, x1, Math.PI, 0);
1366 ctx.lineTo(x3, h - x1 * 4);
1367 ctx.lineTo(x3, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1368 ctx.arc(x2, h - x2, x2, 300 / 360 * 2 * Math.PI, 240 / 360 * 2 * Math.PI);
1369 ctx.lineTo(x1, h - x1 * 2 * (1 + Math.sin(60 / 360 * 2 * Math.PI)));
1374 if (this.dataset.scale === "1") {
1376 ctx.moveTo(x3 + x1 / 2, x1);
1377 ctx.lineTo(x3 + x1, x1);
1378 ctx.moveTo(x3 + x1 / 2, h - 4 * x1);
1379 ctx.lineTo(x3 + x1, h - 4 * x1);
1382 ctx.font = "12px sans-serif";
1383 ctx.fillStyle = "#000000";
1384 ctx.strokeStyle = "#000000";
1385 ctx.textBaseline = "middle";
1387 let mintext = this.dataset.minValue;
1388 let maxtext = this.dataset.maxValue;
1390 if (this.dataset.format !== undefined) {
1391 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1392 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1395 ctx.fillText(mintext, 4.5 * x1, h - 4 * x1, 3.5 * x1);
1396 ctx.fillText(maxtext, 4.5 * x1, x1, 3.5 * x1);
1399 // optional value display
1400 if (this.dataset.printValue === "1") {
1401 ctx.font = "12px sans-serif";
1402 ctx.fillStyle = "#000000";
1403 ctx.strokeStyle = "#000000";
1404 ctx.textBaseline = "bottom";
1405 ctx.textAlign = "center";
1407 let valuetext = this.value;
1409 if (this.dataset.format !== undefined) {
1410 valuetext = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1413 ctx.fillText(valuetext, x2, this.firstChild.height, 4 * x1);
1419function mhttpd_gauge_draw() {
1420 let ctx = this.firstChild.getContext("2d");
1422 let w = this.firstChild.width;
1423 let h = this.firstChild.height;
1425 if (this.dataset.scale === "1")
1429 ctx.clearRect(0, 0, w, h);
1432 if (v < this.dataset.minValue)
1433 v = this.dataset.minValue;
1434 if (v > this.dataset.maxValue)
1435 v = this.dataset.maxValue;
1436 v = (v - this.dataset.minValue) / (this.dataset.maxValue - this.dataset.minValue);
1438 ctx.translate(0.5, 0.5);
1439 ctx.strokeStyle = "#000000";
1440 ctx.fillStyle = "#FFFFFF";
1445 ctx.arc(w / 2, y, w / 2 - 1, Math.PI, 0);
1446 ctx.lineTo(w - 4, y);
1447 ctx.arc(w / 2, y, w / 2 - 4, 0, Math.PI, true);
1449 ctx.fillStyle = this.dataset.color;
1454 ctx.fillStyle = this.dataset.color;
1455 ctx.strokeStyle = this.dataset.color;
1456 ctx.arc(w / 2, y, w / 2 - 6, Math.PI, (1 + v) * Math.PI);
1457 ctx.arc(w / 2, y, w / 2 - w / 5, (1 + v) * Math.PI, Math.PI, true);
1461 if (this.dataset.frame) {
1462 ctx.strokeStyle = "#000000";
1465 ctx.arc(w / 2, y, w / 2 - 6, Math.PI, 0);
1466 ctx.lineTo(w - w / 5, y);
1467 ctx.arc(w / 2, y, w / 2 - w / 5, 0, Math.PI, true);
1472 // optional value display
1473 if (this.dataset.printValue === "1") {
1474 ctx.font = "12px sans-serif";
1475 ctx.fillStyle = "#000000";
1476 ctx.strokeStyle = "#000000";
1477 ctx.textBaseline = "bottom";
1478 ctx.textAlign = "center";
1480 let valtext = this.value;
1482 if (this.dataset.format !== undefined) {
1483 valtext = mie_to_string(parseInt(this.dataset.tid), this.value, this.dataset.format);
1486 ctx.fillText(valtext, w / 2, y, w);
1489 // optional scale display
1490 if (this.dataset.scale === "1") {
1491 ctx.font = "12px sans-serif";
1492 ctx.fillStyle = "#000000";
1493 ctx.strokeStyle = "#000000";
1494 ctx.textBaseline = "bottom";
1495 ctx.textAlign = "center";
1497 let mintext = this.dataset.minValue;
1498 let maxtext = this.dataset.maxValue;
1500 if (this.dataset.format !== undefined) {
1501 mintext = mie_to_string(parseInt(this.dataset.tid), this.dataset.minValue, this.dataset.format);
1502 maxtext = mie_to_string(parseInt(this.dataset.tid), this.dataset.maxValue, this.dataset.format);
1505 ctx.fillText(mintext, 0.1 * w, h, 0.2 * w);
1506 ctx.fillText(maxtext, 0.9 * w, h, 0.2 * w);
1512function mhttpd_vaxis_draw() {
1513 let ctx = this.firstChild.getContext("2d");
1515 let w = this.firstChild.width;
1516 let h = this.firstChild.height;
1517 ctx.clearRect(0, 0, w, h);
1520 if (this.dataset.line === "0")
1523 if (this.dataset.log === "1")
1528 if (this.dataset.minValue !== undefined)
1529 scaleMin = parseFloat(this.dataset.minValue);
1530 if (log && scaleMin === 0)
1532 if (this.dataset.maxValue !== undefined)
1533 scaleMax = parseFloat(this.dataset.maxValue);
1534 if (scaleMin === scaleMax)
1537 ctx.translate(0.5, 0.5);
1538 ctx.strokeStyle = "#000000";
1539 ctx.fillStyle = "#FFFFFF";
1542 if (this.style.textAlign === "left") {
1543 ctx.translate(-0.5, -0.5);
1544 vaxisDraw(ctx, 0, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1546 ctx.translate(-0.5, -0.5);
1547 vaxisDraw(ctx, w, h - 1, h - 2, line, 4, 8, 10, 12, 0, scaleMin, scaleMax, log);
1553function mhttpd_haxis_draw() {
1554 let ctx = this.firstChild.getContext("2d");
1556 let w = this.firstChild.width;
1557 let h = this.firstChild.height;
1558 ctx.clearRect(0, 0, w, h);
1561 if (this.dataset.line === "0")
1564 if (this.dataset.log === "1")
1569 if (this.dataset.minValue !== undefined)
1570 scaleMin = parseFloat(this.dataset.minValue);
1571 if (log && scaleMin === 0)
1573 if (this.dataset.maxValue !== undefined)
1574 scaleMax = parseFloat(this.dataset.maxValue);
1575 if (scaleMin === scaleMax)
1578 ctx.translate(-0.5, 0);
1579 ctx.strokeStyle = "#000000";
1580 ctx.fillStyle = "#FFFFFF";
1583 if (this.style.verticalAlign === "top") {
1584 ctx.translate(0.5, 0.5);
1585 haxisDraw(ctx, 1, 0, w - 2, line, 4, 8, 10, 10, 0, scaleMin, scaleMax, log);
1587 ctx.translate(0.5, -0.5);
1588 haxisDraw(ctx, 1, h, w - 2, line, 4, 8, 10, 20, 0, scaleMin, scaleMax, log);
1593String.prototype.stripZeros = function () {
1594 let s = this.trim();
1595 if (s.search("[.]") >= 0) {
1596 let i = s.search("[e]");
1598 while (s.charAt(i - 1) === "0") {
1599 s = s.substring(0, i - 1) + s.substring(i);
1602 if (s.charAt(i - 1) === ".")
1603 s = s.substring(0, i - 1) + s.substring(i);
1605 while (s.charAt(s.length - 1) === "0")
1606 s = s.substring(0, s.length - 1);
1607 if (s.charAt(s.length - 1) === ".")
1608 s = s.substring(0, s.length - 1);
1614function haxisDraw(ctx, x1, y1, width, line, minor, major, text, label, grid, xmin, xmax, logaxis) {
1615 let dx, int_dx, frac_dx, x_act, label_dx, major_dx, x_screen, maxwidth;
1616 let tick_base, major_base, label_base, n_sig1, n_sig2, xs;
1617 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1619 ctx.textAlign = "center";
1620 ctx.textBaseline = "top";
1622 if (xmax <= xmin || width <= 0)
1626 dx = Math.pow(10, Math.floor(Math.log(xmin) / Math.log(10)));
1633 /* use 6 as min tick distance */
1634 dx = (xmax - xmin) / (width / 6);
1636 int_dx = Math.floor(Math.log(dx) / Math.log(10));
1637 frac_dx = Math.log(dx) / Math.log(10) - int_dx;
1644 tick_base = frac_dx < (Math.log(2) / Math.log(10)) ? 1 : frac_dx < (Math.log(5) / Math.log(10)) ? 2 : 3;
1645 major_base = label_base = tick_base + 1;
1647 /* rounding up of dx, label_dx */
1648 dx = Math.pow(10, int_dx) * base[tick_base];
1649 major_dx = Math.pow(10, int_dx) * base[major_base];
1650 label_dx = major_dx;
1653 /* number of significant digits */
1657 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1662 n_sig2 = Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dx)) / Math.log(10)) + 1;
1664 n_sig1 = Math.max(n_sig1, n_sig2);
1666 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1667 if (Math.abs(xmin) < 100000)
1668 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) + 1);
1669 if (Math.abs(xmax) < 100000)
1670 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(xmax)) / Math.log(10)) + 1);
1672 /* determination of maximal width of labels */
1673 let str = (Math.floor(xmin / dx) * dx).toPrecision(n_sig1);
1674 let ext = ctx.measureText(str);
1675 maxwidth = ext.width;
1677 str = (Math.floor(xmax / dx) * dx).toPrecision(n_sig1).stripZeros();
1678 ext = ctx.measureText(str);
1679 maxwidth = Math.max(maxwidth, ext.width);
1680 str = (Math.floor(xmax / dx) * dx + label_dx).toPrecision(n_sig1).stripZeros();
1681 maxwidth = Math.max(maxwidth, ext.width);
1683 /* increasing label_dx, if labels would overlap */
1684 if (maxwidth > 0.5 * label_dx / (xmax - xmin) * width) {
1686 label_dx = Math.pow(10, int_dx) * base[label_base];
1687 if (label_base % 3 === 2 && major_base % 3 === 1) {
1689 major_dx = Math.pow(10, int_dx) * base[major_base];
1704 x_act = Math.floor(xmin / dx) * dx;
1706 let last_label_x = x1;
1709 ctx.drawLine(x1, y1, x1 + width, y1);
1713 x_screen = (Math.log(x_act) - Math.log(xmin)) / (Math.log(xmax) - Math.log(xmin)) * width + x1;
1715 x_screen = (x_act - xmin) / (xmax - xmin) * width + x1;
1716 xs = Math.floor(x_screen + 0.5);
1718 if (x_screen > x1 + width + 0.001)
1721 if (x_screen >= x1) {
1722 if (Math.abs(Math.floor(x_act / major_dx + 0.5) - x_act / major_dx) <
1723 dx / major_dx / 10.0) {
1725 if (Math.abs(Math.floor(x_act / label_dx + 0.5) - x_act / label_dx) <
1726 dx / label_dx / 10.0) {
1727 /* label tick mark */
1728 ctx.drawLine(xs, y1, xs, y1 + text);
1731 if (grid != 0 && xs > x1 && xs < x1 + width)
1732 ctx.drawLine(xs, y1, xs, y1 + grid);
1736 str = x_act.toPrecision(n_sig1).stripZeros();
1737 ext = ctx.measureText(str);
1739 ctx.fillStyle = "black";
1740 if (xs - ext.width / 2 > x1 &&
1741 xs + ext.width / 2 < x1 + width)
1742 ctx.fillText(str, xs, y1 + label);
1744 last_label_x = xs + ext.width / 2;
1747 /* major tick mark */
1748 ctx.drawLine(xs, y1, xs, y1 + major);
1751 if (grid != 0 && xs > x1 && xs < x1 + width)
1752 ctx.drawLine(xs, y1 - 1, xs, y1 + grid);
1761 /* minor tick mark */
1762 ctx.drawLine(xs, y1, xs, y1 + minor);
1764 /* for logaxis, also put labes on minor tick marks */
1767 str = x_act.toPrecision(n_sig1).stripZeros();
1768 ext = ctx.measureText(str);
1770 ctx.fillStyle = "black";
1771 if (xs - ext.width / 2 > x1 &&
1772 xs + ext.width / 2 < x1 + width &&
1773 xs - ext.width / 2 > last_label_x + 2)
1774 ctx.fillText(str, xs, y1 + label);
1777 last_label_x = xs + ext.width / 2;
1784 /* supress 1.23E-17 ... */
1785 if (Math.abs(x_act) < dx / 100)
1792function vaxisDraw(ctx, x1, y1, height, line, minor, major, text, label, grid, ymin, ymax, logaxis) {
1793 let dy, int_dy, frac_dy, y_act, label_dy, major_dy, y_screen, maxwidth;
1794 let tick_base, major_base, label_base, n_sig1, n_sig2, ys;
1795 let base = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
1798 ctx.textAlign = "right";
1800 ctx.textAlign = "left";
1801 ctx.textBaseline = "middle";
1802 let textHeight = parseInt(ctx.font.match(/\d+/)[0]);
1804 if (ymax <= ymin || height <= 0)
1808 dy = Math.pow(10, Math.floor(Math.log(ymin) / Math.log(10)));
1815 /* use 6 as min tick distance */
1816 dy = (ymax - ymin) / (height / 6);
1818 int_dy = Math.floor(Math.log(dy) / Math.log(10));
1819 frac_dy = Math.log(dy) / Math.log(10) - int_dy;
1826 tick_base = frac_dy < (Math.log(2) / Math.log(10)) ? 1 : frac_dy < (Math.log(5) / Math.log(10)) ? 2 : 3;
1827 major_base = label_base = tick_base + 1;
1829 /* rounding up of dy, label_dy */
1830 dy = Math.pow(10, int_dy) * base[tick_base];
1831 major_dy = Math.pow(10, int_dy) * base[major_base];
1832 label_dy = major_dy;
1834 /* number of significant digits */
1838 n_sig1 = Math.floor(Math.log(Math.abs(xmin)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1843 n_sig2 = Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) - Math.floor(Math.log(Math.abs(label_dy)) / Math.log(10)) + 1;
1845 n_sig1 = Math.max(n_sig1, n_sig2);
1847 // toPrecision displays 1050 with 3 digits as 1.05e+3, so increase presicion to number of digits
1848 if (Math.abs(ymin) < 100000)
1849 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymin)) / Math.log(10)) + 1);
1850 if (Math.abs(ymax) < 100000)
1851 n_sig1 = Math.max(n_sig1, Math.floor(Math.log(Math.abs(ymax)) / Math.log(10)) + 1);
1853 /* increase label_dy if labels would overlap */
1854 while (label_dy / (ymax - ymin) * height < 1.5 * textHeight) {
1856 label_dy = Math.pow(10, int_dy) * base[label_base];
1857 if (label_base % 3 === 2 && major_base % 3 === 1) {
1859 major_dy = Math.pow(10, int_dy) * base[major_base];
1871 y_act = Math.floor(ymin / dy) * dy;
1873 let last_label_y = y1;
1876 ctx.drawLine(x1, y1, x1, y1 - height);
1880 y_screen = y1 - (Math.log(y_act) - Math.log(ymin)) / (Math.log(ymax) - Math.log(ymin)) * height;
1882 y_screen = y1 - (y_act - ymin) / (ymax - ymin) * height;
1883 ys = Math.floor(y_screen + 0.5);
1885 if (y_screen < y1 - height - 0.001)
1888 if (y_screen <= y1 + 0.001) {
1889 if (Math.abs(Math.floor(y_act / major_dy + 0.5) - y_act / major_dy) <
1890 dy / major_dy / 10.0) {
1892 if (Math.abs(Math.floor(y_act / label_dy + 0.5) - y_act / label_dy) <
1893 dy / label_dy / 10.0) {
1894 /* label tick mark */
1895 ctx.drawLine(x1, ys, x1 + text, ys);
1898 if (grid != 0 && ys < y1 && ys > y1 - height)
1899 ctx.drawLine(x1, ys, x1 + grid, ys);
1903 str = y_act.toPrecision(n_sig1).stripZeros();
1905 ctx.fillStyle = "black";
1906 if (ys - textHeight / 2 > y1 - height &&
1907 ys + textHeight / 2 < y1)
1908 ctx.fillText(str, x1 + label, ys);
1910 last_label_y = ys - textHeight / 2;
1913 /* major tick mark */
1914 cts.drawLine(x1, ys, x1 + major, ys);
1917 if (grid != 0 && ys < y1 && ys > y1 - height)
1918 ctx.drawLine(x1, ys, x1 + grid, ys);
1928 /* minor tick mark */
1929 ctx.drawLine(x1, ys, x1 + minor, ys);
1931 /* for logaxis, also put labels on minor tick marks */
1934 str = y_act.toPrecision(n_sig1).stripZeros();
1936 ctx.fillStyle = "black";
1937 if (ys - textHeight / 2 > y1 - height &&
1938 ys + textHeight / 2 < y1 &&
1939 ys + textHeight < last_label_y + 2)
1940 ctx.fillText(str, x1 + label, ys);
1950 /* supress 1.23E-17 ... */
1951 if (Math.abs(y_act) < dy / 100)
1957function mhttpd_resize_sidenav() {
1958 //console.log("mhttpd_resize_sidenav!");
1959 let h = document.getElementById('mheader');
1960 let s = document.getElementById('msidenav');
1961 let top = h.clientHeight + 1 + "px";
1962 if (s.style.top !== top) {
1963 //console.log("httpd_resize_sidenav: top changed from " + s.style.top + " to " + top);
1966 let m = document.getElementById('mmain');
1967 let paddingTop = h.clientHeight + 1 + "px";
1968 if (m.style.paddingTop !== paddingTop) {
1969 //console.log("httpd_resize_sidenav: paddingTop changed from " + m.style.paddingTop + " to " + paddingTop);
1970 m.style.paddingTop = paddingTop;
1974function mhttpd_resize_header() {
1978let mhttpd_last_alarm = 0;
1980function get_options_path(odb_path) {
1981 // Convert "/Path/to/something" to "/Path/to/Options something"
1982 let bits = odb_path.split("/");
1983 bits[bits.length - 1] = "Options " + bits[bits.length - 1];
1984 return bits.join("/");
1987function evalPath(path) {
1989 // if path is a function, evaluate it
1990 if (p && !p.startsWith('/') && p.includes('(') && p.includes(')'))
1995function mhttpd_refresh() {
1997 if (!mhttpd_initialized)
2000 if (mhttpd_refresh_id !== undefined)
2001 window.clearTimeout(mhttpd_refresh_id);
2003 // don't do a refresh if we are paused
2004 if (mhttpd_refresh_paused) {
2005 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2009 // don't update page if document is hidden (minimized, covered tab etc), only play alarms
2010 if (document.hidden) {
2012 // only check every 10 seconds for alarm
2013 if (new Date().getTime() > mhttpd_last_alarm + 10000) {
2015 // request current alarms
2016 let req = mjsonrpc_make_request("get_alarms");
2017 mjsonrpc_send_request([req]).then(function (rpc) {
2019 let alarms = rpc[0].result;
2021 // update alarm display
2022 if (alarms.alarm_system_active) {
2024 for (let a in alarms.alarms)
2028 mhttpd_alarm_play();
2031 }).catch(function (error) {
2032 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2033 mhttpd_error('Connection to server broken. Trying to reconnect ');
2034 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2035 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2037 mjsonrpc_error_alert(error);
2041 mhttpd_last_alarm = new Date().getTime();
2044 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, 500);
2048 // go through all modb* tags and refresh them -------------------------------------
2050 let modb = getMElements("modb");
2052 let modbvalue = getMElements("modbvalue");
2053 for (let i = 0; i < modbvalue.length; i++) {
2055 let o = modbvalue[i];
2056 if (parseInt(o.dataset.input)) {
2057 if (o.childNodes[0] === undefined) {
2058 ODBInlineEdit(o, evalPath(o.dataset.odbPath), 0);
2061 if (parseInt(o.dataset.odbEditable)) {
2062 if (o.childNodes[0] === undefined || o.childNodes[0].tagName === undefined) {
2064 // add link and event handler if tag is editable
2065 let link = document.createElement('a');
2066 link.href = "javascript:void(0);";
2067 link.onclick = function () {
2068 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2070 link.onfocus = function () {
2071 ODBInlineEdit(this.parentElement, evalPath(this.parentElement.dataset.odbPath), 0);
2075 o.appendChild(link);
2079 // remove link if present
2080 if (o.childNodes[0] && o.childNodes[0].tagName === 'A')
2081 o.innerHTML = o.childNodes[0].innerHTML;
2085 modbvalue[i].setValue = function (x, tid) {
2086 if (this.dataset.formula !== undefined)
2087 x = eval(this.dataset.formula);
2088 if (tid === undefined)
2091 this.dataset.tid = tid;
2093 if (this.onchange !== null)
2095 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2097 this.dataset.odbLoaded = "1";
2101 modbvalue[i].sendValueToOdb = function () {
2102 mhttpd_refresh_pause(true);
2103 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2104 mhttpd_refresh_pause(false));
2108 let modbcheckbox = getMElements("modbcheckbox");
2109 for (let i = 0; i < modbcheckbox.length; i++) {
2111 // add event listener if missing
2112 if (!modbcheckbox[i].mEventListener) {
2113 modbcheckbox[i].addEventListener("click", function () {
2114 if (this.dataset.validate !== undefined) {
2115 let flag = eval(this.dataset.validate)(this.checked, this);
2121 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.checked ? 1 : 0).then(rpc =>
2124 modbcheckbox[i].mEventListener = true;
2128 let modbselect = getMElements("modbselect");
2129 for (let i = 0; i < modbselect.length; i++) {
2130 if (modbselect[i].dataset.odbPath) {
2132 // add event listener if missing
2133 if (!modbselect[i].mEventListener) {
2134 modbselect[i].addEventListener("change", function (flag) {
2136 if (flag !== true) { // flag === true if control changed by ODB update
2137 if (this.dataset.validate !== undefined) {
2138 let flag = eval(this.dataset.validate)(this.value, this);
2144 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(rpc =>
2148 modbselect[i].mEventListener = true;
2152 modbselect[i].setValue = function (x) {
2155 if (this.onchange !== null)
2159 modbselect[i].sendValueToOdb = function () {
2160 mhttpd_refresh_pause(true);
2161 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.value).then(() =>
2162 mhttpd_refresh_pause(false));
2167 let modbbox = getMElements("modbbox");
2168 for (let i = 0; i < modbbox.length; i++) {
2169 modbbox[i].style.border = "1px solid #808080";
2172 // attach "set" function to all ODB buttons
2173 let modbbutton = getMElements("modbbutton");
2174 for (let i = 0; i < modbbutton.length; i++) {
2175 if (!modbbutton[i].mEventListener) {
2176 modbbutton[i].addEventListener("click", function () {
2178 if (this.dataset.validate !== undefined) {
2179 let flag = eval(this.dataset.validate)(this);
2186 mjsonrpc_db_set_value(evalPath(this.dataset.odbPath), this.dataset.odbValue).then(rpc =>
2189 modbbutton[i].mEventListener = true;
2193 // replace all horizontal bars with proper <div>'s
2194 let modbhbar = getMElements("modbhbar");
2195 for (let i = 0; i < modbhbar.length; i++) {
2196 if (modbhbar[i].childNodes === undefined || modbhbar[i].childNodes[0] === undefined ||
2197 modbhbar[i].childNodes[0].tagName !== 'DIV') {
2198 modbhbar[i].style.display = "block";
2199 if (modbhbar[i].style.position === "")
2200 modbhbar[i].style.position = "relative";
2201 modbhbar[i].style.border = "1px solid #808080";
2202 let color = modbhbar[i].style.color;
2203 modbhbar[i].innerHTML = "<div style='background-color:" + color + ";" + "color:black;" +
2204 "width:0;height:" + modbhbar[i].clientHeight + "px;" +
2205 "position:relative; display:inline-block;border-right:1px solid #808080'> </div>";
2207 modbhbar[i].setValue = function (x, tid) {
2208 if (this.dataset.formula !== undefined)
2209 x = eval(this.dataset.formula);
2210 if (tid === undefined)
2212 let mvalue = mie_to_string(tid, x, this.dataset.format);
2215 let html = mhttpd_escape(" " + mvalue);
2217 if (this.dataset.printValue === "1")
2218 this.children[0].innerHTML = html;
2219 let minValue = parseFloat(this.dataset.minValue);
2220 let maxValue = parseFloat(this.dataset.maxValue);
2221 if (isNaN(minValue))
2223 if (this.dataset.log === "1" &&
2226 if (isNaN(maxValue))
2229 if (this.dataset.log === "1")
2230 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2231 (Math.log(maxValue) - Math.log(minValue)));
2233 percent = Math.round(100 * (x - minValue) /
2234 (maxValue - minValue));
2239 this.children[0].style.width = percent + "%";
2240 if (this.onchange !== null)
2242 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2244 this.dataset.odbLoaded = "1";
2246 this.children[0].style.backgroundColor = this.style.color;
2251 // replace all vertical bars with proper <div>'s
2252 let modbvbar = getMElements("modbvbar");
2253 for (let i = 0; i < modbvbar.length; i++) {
2254 if (modbvbar[i].childNodes === undefined || modbvbar[i].childNodes[0] === undefined ||
2255 modbvbar[i].childNodes[0].tagName !== 'DIV') {
2256 modbvbar[i].style.display = "inline-block";
2257 if (modbvbar[i].style.position === "")
2258 modbvbar[i].style.position = "relative";
2259 modbvbar[i].style.border = "1px solid #808080";
2260 let color = modbvbar[i].style.color;
2261 modbvbar[i].innerHTML = "<div style='background-color:" + color + "; height:0; width:100%; position:absolute; bottom:0; left:0; display:inline-block; border-top:1px solid #808080'> </div>";
2263 modbvbar[i].setValue = function (x, tid) {
2264 if (this.dataset.formula !== undefined)
2265 x = eval(this.dataset.formula);
2266 if (tid === undefined)
2268 let mvalue = mie_to_string(tid, x, this.dataset.format);
2271 let html = mhttpd_escape(" " + mvalue);
2273 if (this.dataset.printValue === "1")
2274 this.children[0].innerHTML = html;
2275 let minValue = parseFloat(this.dataset.minValue);
2276 let maxValue = parseFloat(this.dataset.maxValue);
2277 if (isNaN(minValue))
2279 if (this.dataset.log === "1" &&
2282 if (isNaN(maxValue))
2285 if (this.dataset.log === "1")
2286 percent = Math.round(100 * (Math.log(x) - Math.log(minValue)) /
2287 (Math.log(maxValue) - Math.log(minValue)));
2289 percent = Math.round(100 * (x - minValue) /
2290 (maxValue - minValue));
2295 this.children[0].style.height = percent + "%";
2296 if (this.onchange !== null)
2298 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2300 this.dataset.odbLoaded = "1";
2302 this.children[0].style.backgroundColor = this.style.color;
2307 // replace all thermometers with canvas
2308 let modbthermo = getMElements("modbthermo");
2309 for (let i = 0; i < modbthermo.length; i++) {
2310 if (modbthermo[i].childNodes === undefined || modbthermo[i].childNodes[0] === undefined ||
2311 modbthermo[i].childNodes[0].tagName !== 'CANVAS') {
2312 modbthermo[i].style.display = "inline-block";
2313 if (modbthermo[i].style.position === "")
2314 modbthermo[i].style.position = "relative";
2316 cvs = document.createElement("canvas");
2317 let w = modbthermo[i].clientWidth;
2318 let h = modbthermo[i].clientHeight;
2319 w = Math.floor(w / 4) * 4; // 2 must be devidable by 4
2322 modbthermo[i].appendChild(cvs);
2323 modbthermo[i].draw = mhttpd_thermo_draw;
2325 modbthermo[i].setValue = function (x, tid) {
2326 if (this.dataset.formula !== undefined)
2327 x = eval(this.dataset.formula);
2328 if (tid === undefined)
2331 this.dataset.tid = tid;
2333 if (this.onchange !== null)
2335 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2337 this.dataset.odbLoaded = "1";
2343 modbthermo[i].draw();
2347 // replace all gauges with canvas
2348 let modbgauge = getMElements("modbgauge");
2349 for (let i = 0; i < modbgauge.length; i++) {
2350 if (modbgauge[i].childNodes === undefined || modbgauge[i].childNodes[0] === undefined ||
2351 modbgauge[i].childNodes[0].tagName !== 'CANVAS') {
2352 modbgauge[i].style.display = "inline-block";
2353 if (modbgauge[i].style.position === "")
2354 modbgauge[i].style.position = "relative";
2356 let cvs = document.createElement("canvas");
2357 cvs.width = modbgauge[i].clientWidth;
2358 cvs.height = modbgauge[i].clientHeight;
2359 modbgauge[i].appendChild(cvs);
2360 modbgauge[i].draw = mhttpd_gauge_draw;
2362 modbgauge[i].setValue = function (x, tid) {
2363 if (this.dataset.formula !== undefined)
2364 x = eval(this.dataset.formula);
2365 if (tid === undefined)
2368 this.dataset.tid = tid;
2370 if (this.onchange !== null)
2372 if (this.dataset.odbLoaded === undefined && this.onload !== null) {
2374 this.dataset.odbLoaded = "1";
2380 modbgauge[i].draw();
2384 // replace all haxis with canvas
2385 let mhaxis = getMElements("mhaxis");
2386 for (let i = 0; i < mhaxis.length; i++) {
2387 if (mhaxis[i].childNodes === undefined || mhaxis[i].childNodes[0] === undefined ||
2388 mhaxis[i].childNodes[0].tagName !== 'CANVAS') {
2389 mhaxis[i].style.display = "block";
2390 if (mhaxis[i].style.position === "")
2391 mhaxis[i].style.position = "relative";
2393 let cvs = document.createElement("canvas");
2394 cvs.width = mhaxis[i].clientWidth + 2;
2395 cvs.height = mhaxis[i].clientHeight;
2396 mhaxis[i].appendChild(cvs);
2397 mhaxis[i].draw = mhttpd_haxis_draw;
2402 // replace all vaxis with canvas
2403 let mvaxis = getMElements("mvaxis");
2404 for (let i = 0; i < mvaxis.length; i++) {
2405 if (mvaxis[i].childNodes === undefined || mvaxis[i].childNodes[0] === undefined ||
2406 mvaxis[i].childNodes[0].tagName !== 'CANVAS') {
2407 mvaxis[i].style.display = "inline-block";
2408 if (mvaxis[i].style.position === "")
2409 mvaxis[i].style.position = "relative";
2411 let cvs = document.createElement("canvas");
2412 cvs.width = mvaxis[i].clientWidth;
2413 cvs.height = mvaxis[i].clientHeight + 2; // leave space for vbar border
2414 mvaxis[i].appendChild(cvs);
2415 mvaxis[i].draw = mhttpd_vaxis_draw;
2420 // replace all mhistory tags with history plots
2421 let mhist = getMElements("mhistory");
2422 for (let i = 0; i < mhist.length; i++) {
2423 if (mhist[i].childNodes === undefined || mhist[i].childNodes[0] === undefined ||
2424 mhist[i].childNodes[0].tagName !== 'IMG') {
2425 let w = mhist[i].style.width;
2430 let h = mhist[i].style.height;
2435 mhist[i].innerHTML = "<img src=\"graph.gif?cmd=oldhistory&group=" +
2436 mhist[i].dataset.group +
2437 "&panel=" + mhist[i].dataset.panel +
2438 "&scale=" + mhist[i].dataset.scale +
2441 "&rnd=" + (new Date().getTime()) +
2446 // request all modb* elements
2448 let modbs = ["modb", "modbvalue", "modbcheckbox", "modbselect", "modbbox", "modbhbar",
2449 "modbvbar", "modbthermo", "modbgauge"];
2451 modbs.forEach((mstr) => {
2452 getMElements(mstr).forEach((m) => {
2453 let p = m.dataset.odbPath;
2456 if (p.includes('[*]'))
2457 p = p.replace('[*]', '');
2461 if (mstr === "modbselect" && m.dataset.autoOptions === "1")
2462 paths.push(get_options_path(evalPath(m.dataset.odbPath)));
2467 // request ODB contents for all variables
2468 let req1 = mjsonrpc_make_request("db_get_values", {"paths": paths});
2470 // request current alarms
2471 let req2 = mjsonrpc_make_request("get_alarms");
2473 // request new messages
2474 let req3 = mjsonrpc_make_request("cm_msg_retrieve", {
2475 "facility": "midas",
2480 // request new char messages
2481 let req4 = mjsonrpc_make_request("cm_msg_retrieve", {
2487 mjsonrpc_send_request([req1, req2, req3, req4]).then(function (rpc) {
2489 // update time in header
2490 let dstr = mhttpd_get_display_time().string;
2492 if (document.getElementById("mheader_last_updated") !== undefined) {
2493 //mhttpd_set_innerHTML(document.getElementById("mheader_last_updated"), dstr);
2494 mhttpd_set_firstChild_data(document.getElementById("mheader_last_updated"), dstr);
2499 for (let i = 0; i < modb.length; i++, idata++) {
2500 if (modb[i].dataset.odbPath === undefined) {
2504 let x = rpc[0].result.data[idata];
2505 if (typeof x === 'object' && x !== null) {
2507 if (modb[i].json_value === undefined)
2508 modb[i].json_value = JSON.stringify(x);
2509 if (modb[i].onchange !== null && JSON.stringify(x) !== modb[i].json_value) {
2510 modb[i].json_value = JSON.stringify(x);
2514 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2516 modb[i].dataset.odbLoaded = "1";
2520 if (modb[i].value === undefined)
2522 if (modb[i].onchange !== null && x !== modb[i].value) {
2526 if (modb[i].dataset.odbLoaded === undefined && modb[i].onload !== null) {
2528 modb[i].dataset.odbLoaded = "1";
2533 for (let i = 0; i < modbvalue.length; i++, idata++) {
2534 if (modbvalue[i].dataset.odbPath === undefined) {
2539 if (rpc[0].result.status[idata] === 312) {
2540 modbvalue[i].innerHTML = "ODB key \"" +
2541 evalPath(modbvalue[i].dataset.odbPath) + "\" not found";
2543 let x = rpc[0].result.data[idata];
2544 let tid = rpc[0].result.tid[idata];
2546 modbvalue[i].tid = tid;
2547 if (modbvalue[i].dataset.formula !== undefined)
2548 x = eval(modbvalue[i].dataset.formula);
2549 let mvalue = mie_to_string(tid, x, modbvalue[i].dataset.format);
2552 else if (typeof mvalue === 'string' && mvalue.trim() === "")
2553 mvalue = "(spaces)";
2555 if (tid === TID_BOOL)
2558 html = mhttpd_escape(mvalue);
2559 if (parseInt(modbvalue[i].dataset.odbEditable) || parseInt(modbvalue[i].dataset.input)) {
2560 if (modbvalue[i].childNodes[1] !== undefined &&
2561 modbvalue[i].childNodes[1].innerHTML !== undefined) {
2562 modbvalue[i].childNodes[1].innerHTML = html;
2564 if (modbvalue[i].childNodes[0]) // dataset.input could not yet be initialized (AJAX)
2565 modbvalue[i].childNodes[0].innerHTML = html;
2568 modbvalue[i].innerHTML = html;
2570 if (modbvalue[i].value !== x && modbvalue[i].onchange !== null) {
2571 modbvalue[i].value = x;
2572 if (modbvalue[i].value !== undefined)
2573 modbvalue[i].onchange();
2576 modbvalue[i].value = x;
2578 if (modbvalue[i].dataset.odbLoaded === undefined && modbvalue[i].onload !== null) {
2579 modbvalue[i].onload();
2580 modbvalue[i].dataset.odbLoaded = "1";
2585 for (let i = 0; i < modbcheckbox.length; i++, idata++) {
2586 if (modbcheckbox[i].dataset.odbPath === undefined) {
2591 let x = rpc[0].result.data[idata];
2592 let tid = rpc[0].result.tid[idata];
2593 let mvalue = mie_to_string(tid, x);
2594 if (typeof x === "boolean")
2595 modbcheckbox[i].checked = x;
2597 modbcheckbox[i].checked = (mvalue !== "0");
2598 if (modbcheckbox[i].onchange !== null)
2599 modbcheckbox[i].onchange();
2600 if (modbcheckbox[i].dataset.odbLoaded === undefined && modbcheckbox[i].onload !== null) {
2601 modbcheckbox[i].onload();
2602 modbcheckbox[i].dataset.odbLoaded = "1";
2606 for (let i = 0; i < modbselect.length; i++, idata++) {
2607 if (modbselect[i].dataset.odbPath === undefined) {
2612 let x = rpc[0].result.data[idata];
2613 let auto_options = [];
2615 if (modbselect[i].dataset.autoOptions == "1") {
2617 auto_options = rpc[0].result.data[idata];
2619 if (auto_options !== null && modbselect[i].options.length == 0) {
2620 for (let opt = 0; opt < auto_options.length; opt++) {
2621 modbselect[i].add(new Option(auto_options[opt], auto_options[opt]));
2626 if (rpc[0].result.status[idata] === 312) {
2627 modbselect[i].parentElement.innerHTML = "ODB key \"" +
2628 evalPath(modbselect[i].dataset.odbPath) + "\" not found";
2630 if (x !== null && modbselect[i].value !== x.toString()) {
2632 if (modbselect[i].value !== undefined) {
2633 // check if option is available
2635 for (j = 0; j < modbselect[i].options.length; j++)
2636 if (modbselect[i].options[j].value === x.toString())
2639 // if option is available, set it
2640 if (j < modbselect[i].options.length) {
2641 modbselect[i].value = x.toString();
2642 if (modbselect[i].onchange !== null)
2643 modbselect[i].onchange(true);
2645 // if not, set blank
2646 if (modbselect[i].value !== "")
2647 modbselect[i].value = "";
2651 if (Array.isArray(x)) {
2652 // check if all array values are the same
2655 modbselect[i].value = s.values().next().value;
2657 modbselect[i].value = "...";
2660 if (modbselect[i].dataset.odbLoaded === undefined && modbselect[i].onload !== null) {
2661 modbselect[i].onload();
2662 modbselect[i].dataset.odbLoaded = "1";
2667 for (let i = 0; i < modbbox.length; i++, idata++) {
2668 if (modbbox[i].dataset.odbPath === undefined) {
2673 let x = rpc[0].result.data[idata];
2674 if (modbbox[i].dataset.formula !== undefined)
2675 x = eval(modbbox[i].dataset.formula);
2676 if (x > 0 || x === true) {
2677 modbbox[i].style.backgroundColor = modbbox[i].dataset.color;
2679 if (modbbox[i].dataset.backgroundColor !== undefined)
2680 modbbox[i].style.backgroundColor = modbbox[i].dataset.backgroundColor;
2682 modbbox[i].style.backgroundColor = "";
2684 if (modbbox[i].onchange !== null)
2685 modbbox[i].onchange();
2686 if (modbbox[i].dataset.odbLoaded === undefined && modbbox[i].onload !== null) {
2687 modbbox[i].onload();
2688 modbbox[i].dataset.odbLoaded = "1";
2692 for (let i = 0; i < modbhbar.length; i++, idata++) {
2693 if (modbhbar[i].dataset.odbPath === undefined) {
2698 let x = rpc[0].result.data[idata];
2699 let tid = rpc[0].result.tid[idata];
2701 modbhbar[i].setValue(x, tid);
2704 for (let i = 0; i < modbvbar.length; i++, idata++) {
2705 if (modbvbar[i].dataset.odbPath === undefined) {
2710 let x = rpc[0].result.data[idata];
2711 let tid = rpc[0].result.tid[idata];
2713 modbvbar[i].setValue(x, tid);
2716 for (let i = 0; i < modbthermo.length; i++, idata++) {
2717 if (modbthermo[i].dataset.odbPath === undefined) {
2722 let x = rpc[0].result.data[idata];
2723 let tid = rpc[0].result.tid[idata];
2725 modbthermo[i].setValue(x, tid);
2728 for (let i = 0; i < modbgauge.length; i++, idata++) {
2729 if (modbgauge[i].dataset.odbPath === undefined) {
2734 let x = rpc[0].result.data[idata];
2735 let tid = rpc[0].result.tid[idata];
2737 modbgauge[i].setValue(x, tid);
2740 let alarms = rpc[1].result;
2742 // update alarm display
2743 let e = document.getElementById('mheader_alarm');
2744 if (!alarms.alarm_system_active) {
2745 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: Off</a>");
2746 mhttpd_set_className(e, "mgray mbox");
2751 for (let a in alarms.alarms) {
2755 if (alarms.alarms[a].alarm_sound)
2759 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: None</a>");
2760 mhttpd_set_className(e, "mgreen mbox");
2764 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarms: " + s + "</a>");
2766 mhttpd_set_innerHTML(e, "<a href=\"?cmd=Alarms\">Alarm: " + s + "</a>");
2767 mhttpd_set_className(e, "mred mbox");
2770 mhttpd_alarm_play();
2776 if (rpc[2].result.messages !== undefined) {
2777 msg = rpc[2].result.messages.split("\n");
2778 if (msg[msg.length - 1] === "")
2779 msg = msg.slice(0, -1);
2783 // update chat messages
2785 if (rpc[3].result.messages !== undefined) {
2786 chat = rpc[3].result.messages.split("\n");
2787 if (chat[chat.length - 1] === "")
2788 chat = chat.slice(0, -1);
2792 mhttpd_message(msg, chat);
2793 mhttpd_resize_sidenav();
2795 if (mhttpd_refresh_interval !== undefined && mhttpd_refresh_interval > 0)
2796 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2798 }).catch(function (error) {
2800 if (error.xhr && (error.xhr.readyState === 4) && ((error.xhr.status === 0) || (error.xhr.status === 503))) {
2801 mhttpd_error('Connection to server broken. Trying to reconnect ');
2802 document.getElementById("mheader_error").appendChild(mhttpd_spinning_wheel);
2803 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2805 mjsonrpc_error_alert(error);
2810function mhttpd_refresh_history() {
2811 if (mhttpd_refresh_history_id !== undefined)
2812 window.clearTimeout(mhttpd_refresh_history_id);
2814 /* this fuction gets called by mhttpd_init to periodically refresh all history panels */
2816 let mhist = document.getElementsByName("mhistory");
2817 for (let i = 0; i < mhist.length; i++) {
2818 let s = mhist[i].childNodes[0].src;
2820 if (s.lastIndexOf("&rnd=") !== -1) {
2821 s = s.substr(0, s.lastIndexOf('&rnd='));
2822 s += "&rnd=" + new Date().getTime();
2824 mhist[i].childNodes[0].src = s;
2827 if (mhttpd_refresh_history_interval !== undefined && mhttpd_refresh_history_interval > 0)
2828 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2831function mhttpd_reconnect() {
2832 mjsonrpc_db_ls(["/"]).then(function (rpc) {
2833 // on successful connection remove error and schedule refresh
2834 mhttpd_error_clear();
2835 if (mhttpd_refresh_id !== undefined)
2836 window.clearTimeout(mhttpd_refresh_id);
2837 if (mhttpd_refresh_history_id !== undefined)
2838 window.clearTimeout(mhttpd_refresh_history_id);
2839 mhttpd_refresh_id = window.setTimeout(mhttpd_refresh, mhttpd_refresh_interval);
2840 mhttpd_refresh_history_id = window.setTimeout(mhttpd_refresh_history, mhttpd_refresh_history_interval);
2841 }).catch(function (error) {
2842 mhttpd_reconnect_id = window.setTimeout(mhttpd_reconnect, 1000);
2846window.addEventListener('resize', mhttpd_resize_message);
2848function mhttpd_resize_message() {
2849 //console.log("mhttpd_resize_message() via resize event listener");
2850 let d = document.getElementById("mheader_message");
2851 if (d.currentMessage !== undefined && d.style.display !== 'none')
2852 mhttpd_fit_message(d.currentMessage);
2855function mhttpd_close_message() {
2856 let d = document.getElementById("mheader_message");
2858 // remember time of messages to suppress
2859 mhttpdConfigSet('suppressMessageBefore', d.currentMessageT);
2861 d.style.display = "none";
2862 mhttpd_resize_sidenav();
2865function mhttpd_fit_message(m) {
2866 let d = document.getElementById("mheader_message");
2867 let cross = " <span style=\"cursor: pointer;\" onclick=\"mhttpd_close_message();\">╳</span>";
2868 let link1 = "<span style=\"cursor: pointer;\" onclick=\"window.location.href='?cmd=Messages'\">";
2869 let link2 = "</span>";
2870 d.style.display = "inline-block";
2872 // limit message to fit parent element
2874 let parentWidth = d.parentNode.offsetWidth;
2875 let maxWidth = parentWidth - 30;
2877 // check if the full message fits
2879 d.innerHTML = link1 + m + link2 + cross;
2880 //console.log("mhttpd_fit_message: len: " + d.offsetWidth + ", max: " + maxWidth + ", message: " + m);
2881 if (d.offsetWidth <= maxWidth) {
2885 // check if the message minus timestamp and type fits
2887 m = m.substr(m.indexOf(']')+1);
2888 d.innerHTML = link1 + m + link2 + cross;
2889 let w = d.offsetWidth;
2890 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + m);
2891 if (w <= maxWidth) {
2895 // guess the length assuming fix pixels per char
2897 let charWidth = w/m.length;
2898 let guessLength = maxWidth/charWidth - 3; // 3 chars of "..."
2900 let g = m.substr(0, guessLength);
2901 d.innerHTML = link1 + g + "..." + link2 + cross;
2903 //console.log("mhttpd_fit_message: char: " + charWidth + ", guess: " + guessLength + ", len: " + w + ", max: " + maxWidth);
2905 // grow or shrink our guess
2908 //console.log("mhttpd_fit_message: too short, grow");
2909 for (let i=guessLength+1; i<=m.length; i++) {
2910 let s = m.substr(0, i);
2911 d.innerHTML = link1 + s + "..." + link2 + cross;
2913 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + s);
2918 //console.log("mhttpd_fit_message: too long, shrink");
2919 while (g.length > 0) {
2920 g = g.substr(0, g.length-1);
2921 d.innerHTML = link1 + g + "..." + link2 + cross;
2923 //console.log("mhttpd_fit_message: len: " + w + ", max: " + maxWidth + ", message: " + g);
2930function mhttpd_message(msg, chat) {
2945 if (msg !== undefined) {
2946 lastMsg = msg[0].substr(msg[0].indexOf(" ") + 1);
2947 lastMsgT = parseInt(msg[0]);
2950 if (chat !== undefined) {
2951 lastChat = chat[0].substr(chat[0].indexOf(" ") + 1);
2952 lastChatT = parseInt(chat[0]);
2953 if (chat[0].length > 0)
2954 mTalk = chat[0].substr(chat[0].indexOf("]") + 2);
2956 chatName = lastChat.substring(lastChat.indexOf("[") + 1, lastChat.indexOf(","));
2957 lastChat = lastChat.substr(0, lastChat.indexOf("[")) +
2958 "<b>" + chatName + ":</b>" +
2959 lastChat.substr(lastChat.indexOf("]") + 1);
2962 if (lastChatT > lastMsgT) {
2966 talkTime = lastChatT;
2967 notifyTime = lastChatT;
2971 c = "var(--myellow)";
2972 mTalk = lastMsg.substr(lastMsg.indexOf("]") + 1);
2973 mType = m.substring(m.indexOf(",") + 1, m.indexOf("]"));
2974 talkTime = lastMsgT;
2975 notifyTime = lastMsgT;
2980 let d = document.getElementById("mheader_message");
2981 if (d !== undefined && d !== null && d.currentMessage !== m &&
2982 (mhttpdConfig().suppressMessageBefore === undefined || lastT > mhttpdConfig().suppressMessageBefore)) {
2984 d.style.removeProperty("-webkit-transition");
2985 d.style.removeProperty("transition");
2987 if (mType === "USER" && mhttpdConfig().displayChat ||
2988 mType === "TALK" && mhttpdConfig().displayTalk ||
2989 mType === "ERROR" && mhttpdConfig().displayError ||
2990 mType === "INFO" && mhttpdConfig().displayInfo ||
2991 mType === "LOG" && mhttpdConfig().displayLog) {
2993 let first = (d.currentMessage === undefined);
2994 d.currentMessage = m; // store full message in user-defined attribute
2995 d.currentMessageT = lastMsgT; // store message time in user-defined attribute
2997 mhttpd_fit_message(m);
2998 d.age = new Date() / 1000;
3001 if (m.search("ERROR]") > 0) {
3002 d.style.backgroundColor = "var(--mred)";
3003 d.style.color = "white";
3007 // manage backgroud color (red for errors, fading yellow for others)
3008 if (m.search("ERROR]") > 0) {
3009 d.style.removeProperty("-webkit-transition");
3010 d.style.removeProperty("transition");
3011 d.style.backgroundColor = "var(--mred)";
3013 d.age = new Date() / 1000;
3014 d.style.removeProperty("-webkit-transition");
3015 d.style.removeProperty("transition");
3016 d.style.backgroundColor = c;
3022 if (mType === "USER" && mhttpdConfig().speakChat) {
3023 // do not speak own message
3024 if (document.getElementById("chatName") === undefined ||
3025 document.getElementById("chatName") === null ||
3026 document.getElementById("chatName").value !== chatName) {
3027 mhttpd_speak(talkTime, mTalk);
3029 } else if (mType === "TALK" && mhttpdConfig().speakTalk) {
3030 mhttpd_speak(talkTime, mTalk);
3031 } else if (mType === "ERROR" && mhttpdConfig().speakError) {
3032 mhttpd_speak(talkTime, mTalk);
3033 } else if (mType === "INFO" && mhttpdConfig().speakInfo) {
3034 mhttpd_speak(talkTime, mTalk);
3035 } else if (mType === "LOG" && mhttpdConfig().speakLog) {
3036 mhttpd_speak(talkTime, mTalk);
3040 if (mType === "USER" && mhttpdConfig().notifyChat ||
3041 mType === "TALK" && mhttpdConfig().notifyTalk ||
3042 mType === "ERROR" && mhttpdConfig().notifyError ||
3043 mType === "INFO" && mhttpdConfig().notifyInfo ||
3044 mType === "LOG" && mhttpdConfig().notifyLog) {
3046 // avoid double notification if page gets reloaded
3047 // console.log("notifyTime: " + notifyTime + " last: " + mhttpdConfig().var.lastNotify);
3048 if (mhttpdConfig().var.lastNotify === undefined || notifyTime > mhttpdConfig().var.lastNotify) {
3049 mhttpdConfigSet("var.lastNotify", notifyTime);
3051 if (("Notification" in window)) {
3052 if (Notification.permission === "granted") {
3053 const n = new Notification("MIDAS", {
3055 icon: "./apple-touch-icon.png"
3057 } else if (Notification.permission !== "denied") {
3058 Notification.requestPermission().then((p) => {
3059 if (p === "granted") {
3060 const n = new Notification("MIDAS", {
3062 icon: "./apple-touch-icon.png"
3073 let t = new Date() / 1000;
3074 if (t > d.age + 5 && d.style.backgroundColor === "var(--myellow)") {
3075 let backgroundColor = "var(--mgray)";
3076 d.style.setProperty("-webkit-transition", "background-color 3s", "");
3077 d.style.setProperty("transition", "background-color 3s", "");
3078 d.style.backgroundColor = backgroundColor;
3083function mhttpd_error(error) {
3084 let d = document.getElementById("mheader_error");
3085 if (d !== undefined) {
3086 error += "<div style=\"display: inline; float: right; padding-right: 10px; cursor: pointer;\"" +
3087 " onclick=\"document.getElementById("mheader_error").style.zIndex = 0;\">╳</div>";
3088 d.innerHTML = error;
3089 d.style.zIndex = 3; // above header
3093function mhttpd_error_clear() {
3094 if (document.getElementById("mheader_error")) {
3095 document.getElementById("mheader_error").innerHTML = "";
3096 document.getElementById("mheader_error").style.zIndex = 0; // below header
3100function mhttpd_create_page_handle_create(mouseEvent) {
3104 let arraylength = "";
3105 let stringlength = "";
3107 let form = document.getElementsByTagName('form')[0];
3110 path = form.elements['odb'].value;
3111 type = form.elements['type'].value;
3112 name = form.elements['value'].value;
3113 arraylength = form.elements['index'].value;
3114 stringlength = form.elements['strlen'].value;
3116 let e = document.getElementById("odbpath");
3117 path = JSON.parse(e.innerHTML);
3118 if (path === "/") path = "";
3120 type = document.getElementById("create_tid").value;
3121 name = document.getElementById("create_name").value;
3122 arraylength = document.getElementById("create_array_length").value;
3123 stringlength = document.getElementById("create_strlen").value;
3125 //alert("Path: " + path + " Name: " + name);
3128 if (path === "/") path = "";
3130 if (name.length < 1) {
3131 dlgAlert("Name is too short");
3135 let int_array_length = parseInt(arraylength);
3137 //alert("int_array_length: " + int_array_length);
3139 if (!int_array_length || int_array_length < 1) {
3140 dlgAlert("Bad array length: " + arraylength);
3144 let int_string_length = parseInt(stringlength);
3146 if (!int_string_length || int_string_length < 1) {
3147 dlgAlert("Bad string length " + stringlength);
3152 param.path = path + "/" + name;
3153 param.type = parseInt(type);
3154 if (int_array_length > 1)
3155 param.array_length = int_array_length;
3156 if (int_string_length > 0)
3157 param.string_length = int_string_length;
3159 mjsonrpc_db_create([param]).then(function (rpc) {
3160 let status = rpc.result.status[0];
3161 if (status === 311) {
3162 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3163 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3165 } else if (status !== 1) {
3166 dlgMessage("Error", "db_create_key() error " + status + ", see MIDAS messages.", true, true, function () {
3167 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3170 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3172 }).catch(function (error) {
3173 mjsonrpc_error_alert(error);
3174 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3180function mhttpd_create_page_handle_cancel(mouseEvent) {
3181 dlgHide('dlgCreate');
3185function mhttpd_link_page_handle_link(mouseEvent) {
3186 let e = document.getElementById("link_odbpath");
3187 let path = JSON.parse(e.innerHTML);
3188 if (path === "/") path = "";
3189 //let path = document.getElementById("odb_path").value;
3190 let name = document.getElementById("link_name").value;
3191 let target = document.getElementById("link_target").value;
3193 //console.log("Path: " + path + " Name: " + name + " Target: [" + target + "]");
3195 if (name.length < 1) {
3196 dlgAlert("Name is too short");
3200 if (target.length <= 1) {
3201 dlgAlert("Link target is too short");
3206 param.new_links = [path + "/" + name];
3207 param.target_paths = [target];
3209 mjsonrpc_call("db_link", param).then(function (rpc) {
3210 let status = rpc.result.status[0];
3211 if (status === 304) {
3212 dlgMessage("Error", "Invalid link, see MIDAS messages.", true, true, function () {
3213 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3215 } else if (status === 311) {
3216 dlgMessage("Error", "ODB entry with this name already exists.", true, true, function () {
3217 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3219 } else if (status === 312) {
3220 dlgMessage("Error", "Target path " + target + " does not exist in ODB.", true, true, function () {
3221 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3223 } else if (status === 315) {
3224 dlgMessage("Error", "ODB data type mismatch, see MIDAS messages.", true, true, function () {
3225 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3227 } else if (status !== 1) {
3228 dlgMessage("Error", "db_create_link() error " + status + ", see MIDAS messages.", true, true, function () {
3229 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3232 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3234 }).catch(function (error) {
3235 mjsonrpc_error_alert(error);
3236 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3242function mhttpd_link_page_handle_cancel(mouseEvent) {
3247function mhttpd_delete_page_handle_delete(mouseEvent, xpath) {
3248 let form = document.getElementsByTagName('form')[0];
3253 path = form.elements['odb'].value;
3255 if (path === "/") path = "";
3257 for (let i = 0; ; i++) {
3259 let v = form.elements[n];
3260 if (v === undefined)
3263 names.push(path + "/" + v.value);
3266 let e = document.getElementById("odbpath");
3267 path = JSON.parse(e.innerHTML);
3268 if (path === "/") path = "";
3270 //alert("Path: " + path);
3272 for (i = 0; ; i++) {
3273 let v = document.getElementById("delete" + i);
3274 if (v === undefined || v === null)
3277 let name = JSON.parse(v.value);
3278 if (name.length > 0) {
3279 names.push(path + "/" + name);
3284 //alert("Names: " + names);
3288 if (names.length < 1) {
3289 dlgAlert("Please select at least one ODB entry to delete.");
3296 params.paths = names;
3297 mjsonrpc_call("db_delete", params).then(function (rpc) {
3299 let status = rpc.result.status;
3300 //alert(JSON.stringify(status));
3301 for (let i = 0; i < status.length; i++) {
3302 if (status[i] !== 1) {
3303 message += "Cannot delete \"" + rpc.request.params.paths[i] + "\", db_delete_key() status " + status[i] + "\n";
3306 if (message.length > 0) {
3309 location.search = "?cmd=odb&odb_path=" + path; // reloads the document
3311 }).catch(function (error) {
3312 mjsonrpc_error_alert(error);
3313 //location.search = "?cmd=odb&odb_path="+path; // reloads the document
3319function mhttpd_delete_page_handle_cancel(mouseEvent) {
3320 dlgHide('dlgDelete');
3324function mhttpd_start_run(ret) {
3325 mhttpd_goto_page("Start", ret); // DOES NOT RETURN
3328function mhttpd_stop_run(ret) {
3329 dlgConfirm('Are you sure to stop the run?', function (flag) {
3330 if (flag === true) {
3331 mjsonrpc_call("cm_transition", {"transition": "TR_STOP"}).then(function (rpc) {
3332 //mjsonrpc_debug_alert(rpc);
3333 if (rpc.result.status !== 1) {
3334 throw new Error("Cannot stop run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3336 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3337 }).catch(function (error) {
3338 mjsonrpc_error_alert(error);
3345function mhttpd_pause_run(ret) {
3346 dlgConfirm('Are you sure to pause the run?', function (flag) {
3347 if (flag === true) {
3348 mjsonrpc_call("cm_transition", {"transition": "TR_PAUSE"}).then(function (rpc) {
3349 //mjsonrpc_debug_alert(rpc);
3350 if (rpc.result.status !== 1) {
3351 throw new Error("Cannot pause run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3353 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3354 }).catch(function (error) {
3355 mjsonrpc_error_alert(error);
3362function mhttpd_resume_run(ret) {
3363 dlgConfirm('Are you sure to resume the run?', function (flag) {
3364 if (flag === true) {
3365 mjsonrpc_call("cm_transition", {"transition": "TR_RESUME"}).then(function (rpc) {
3366 //mjsonrpc_debug_alert(rpc);
3367 if (rpc.result.status !== 1) {
3368 throw new Error("Cannot resume run, cm_transition() status " + rpc.result.status + ", see MIDAS messages");
3370 mhttpd_goto_page("Transition", ret); // DOES NOT RETURN
3371 }).catch(function (error) {
3372 mjsonrpc_error_alert(error);
3378function mhttpd_cancel_transition() {
3379 dlgConfirm('Are you sure to cancel the currently active run transition?', function (flag) {
3380 if (flag === true) {
3384 paths.push("/Runinfo/Requested Transition");
3386 paths.push("/Runinfo/Transition in progress");
3390 params.paths = paths;
3391 params.values = values;
3393 mjsonrpc_call("db_paste", params).then(function (rpc) {
3394 //mjsonrpc_debug_alert(rpc);
3395 if ((rpc.result.status[0] !== 1) || (rpc.result.status[1] !== 1)) {
3396 throw new Error("Cannot cancel transition, db_paste() status " + rpc.result.status + ", see MIDAS messages");
3398 mhttpd_goto_page("Transition"); // DOES NOT RETURN
3399 }).catch(function (error) {
3400 mjsonrpc_error_alert(error);
3406function mhttpd_reset_alarm(alarm_name) {
3407 mjsonrpc_call("al_reset_alarm", {"alarms": [alarm_name]}).then(function (rpc) {
3408 //mjsonrpc_debug_alert(rpc);
3409 if (rpc.result.status[0] !== 1 && rpc.result.status[0] !== 1004) {
3410 throw new Error("Cannot reset alarm, status " + rpc.result.status + ", see MIDAS messages");
3412 }).catch(function (error) {
3413 mjsonrpc_error_alert(error);
3417/*---- site and session storage ----------------------------*/
3422 flag = mhttpdConfig().speakChat; // read
3424 mhttpdConfigSet('speakChat', false); // write individual config
3426 let c = mhttpdConfig(); // write whole config
3427 c.speakChat = false;
3429 mhttpdConfigSetAll(c);
3432 Saves settings are kept in local storage, which gets
3433 cleared when the browser session ends. Then the default
3434 values are returned.
3437let mhttpd_config_defaults = {
3445 'displayChat': true,
3446 'displayTalk': true,
3447 'displayError': true,
3448 'displayInfo': false,
3449 'displayLog': false,
3453 'speakError': false,
3457 'displayChat': true,
3458 'displayTalk': true,
3459 'displayError': true,
3460 'displayInfo': true,
3461 'displayLog': false,
3463 'speakVoice': 'Alex',
3467 'alarmSoundFile': 'beep.mp3',
3471 'timezone': 'local',
3478 'suppressMessageBefore': 0,
3481 'facility': 'midas',
3486function mhttpdConfigODB(callback) {
3487 let c = mhttpd_config_defaults;
3489 if (localStorage.mhttpd) {
3490 c = JSON.parse(localStorage.mhttpd);
3493 // obtain some defaults from ODB
3494 mjsonrpc_db_get_value("/Experiment/Enable sound").then(function (rpc) {
3495 let flag = rpc.result.data[0];
3496 if (flag === null) {
3497 mjsonrpc_db_create([{"path" : "/Experiment/Enable sound", "type" : TID_BOOL}]).then(function (rpc) {
3498 mjsonrpc_db_paste(["/Experiment/Enable sound"],[true]).then(function(rpc) {}).catch(function(error) {
3499 mjsonrpc_error_alert(error); });
3500 }).catch(function (error) {
3501 mjsonrpc_error_alert(error);
3503 } else if (flag === false) {
3504 c.speakChat = false;
3505 c.speakTalk = false;
3506 c.speakError = false;
3507 c.speakInfo = false;
3509 c.alarmSound = false;
3510 localStorage.setItem('mhttpd', JSON.stringify(c));
3513 }).catch(function (error) {
3514 mjsonrpc_error_alert(error);
3521function mhttpdConfig() {
3522 let c = mhttpd_config_defaults;
3524 if (localStorage.mhttpd)
3525 c = JSON.parse(localStorage.mhttpd);
3527 // if element has been added to mhttpd_config_defaults, merge it
3528 if (Object.keys(c).length !== Object.keys(mhttpd_config_defaults).length) {
3529 for (let o in mhttpd_config_defaults)
3531 c[o] = mhttpd_config_defaults[o];
3539function mhttpdConfigSet(item, value) {
3541 let c = mhttpdConfig();
3542 if (item.indexOf('.') > 0) {
3543 let c1 = item.substring(0, item.indexOf('.'));
3544 let c2 = item.substring(item.indexOf('.') + 1);
3548 localStorage.setItem('mhttpd', JSON.stringify(c));
3553function mhttpdConfigSetAll(new_config) {
3555 localStorage.setItem('mhttpd', JSON.stringify(new_config));
3560/*---- sound and speak functions --------------------------*/
3562let last_audio = null;
3563let inside_new_audio = false;
3567// Do not delete the following code. It is not used at this moment,
3568// but may become necessary in the future. Included
3569// is an alternative method of allocating and releasing Audio objects
3570// and code for tracing Audio object life time and activity. It is needed
3571// to debug interactions between Audio objects and throttled javascript code
3572// in inactive tabs; and with the "user did not interact with this tab" business.
3575//function mhttpd_alarm_done() {
3578// ended = last_audio.ended;
3579// last_audio = null;
3581// count_audio_done++;
3582// console.log(Date() + ": mhttpd_alarm_done: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended);
3585//function mhttpd_audio_loadeddata(e) {
3586// console.log(Date() + ": mhttpd_audio_loadeddata: counter " + e.target.counter);
3589//function mhttpd_audio_canplay(e) {
3590// console.log(Date() + ": mhttpd_audio_canplay: counter " + e.target.counter);
3593//function mhttpd_audio_canplaythrough(e) {
3594// console.log(Date() + ": mhttpd_audio_canplaythrough: counter " + e.target.counter);
3597//function mhttpd_audio_ended(e) {
3598// console.log(Date() + ": mhttpd_audio_ended: counter " + e.target.counter);
3601//function mhttpd_audio_paused(e) {
3602// console.log(Date() + ": mhttpd_audio_paused: counter " + e.target.counter);
3605function mhttpd_alarm_play_now() {
3609 // check for playing the alarm sound is done every few minutes.
3610 // it takes 3 seconds to play the alarm sound
3611 // so in theory, this check is not needed: previous alarm sound should
3612 // always have finished by the time we play a new sound.
3614 // However, observed with google-chrome, in inactive and/or iconized tabs
3615 // javascript code is "throttled" and the above time sequence is not necessarily
3618 // Specifically, I see the following sequence: after 1-2 days of inactivity,
3619 // (i.e.) the "programs" page starts consuming 10-15% CPU, if I open it,
3620 // CPU use goes to 100% for a short while, memory use goes from ~50 Mbytes
3621 // to ~150 Mbytes. console.log() debug print out shows that there are ~400
3622 // accumulated Audio() objects that have been created but did not finish playing
3623 // (because google-chrome is throttling javascript in inactive tabs?), and now
3624 // they all try to load the mp3 file and play all at the same time.
3626 // This fix for this observed behaviour is to only create new Audio() objects
3627 // if previous one finished playing.
3631 if (!last_audio.ended) {
3632 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: previous alarm sound did not finish playing yet");
3637 if (inside_new_audio) {
3638 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: already inside \"new Audio()\"");
3641 inside_new_audio = true;
3643 // reference counting of Audio objects. Not in use today. Do not remove. K.O. Jan 2021
3644 //console.log(Date() + ": mhttpd_alarm_play: created: " + count_audio_created + ", done: " + count_audio_done + ", last_ended: " + ended + ", audio.play!");
3645 //count_audio_created++;
3647 let audio = new Audio(mhttpdConfig().alarmSoundFile);
3648 audio.volume = mhttpdConfig().alarmVolume;
3649 audio.counter = ++count_audio;
3652 inside_new_audio = false;
3654 // code for tracing activity of Audio objects. do not remove. K.O. Jan 2021
3655 //audio.addEventListener("loadeddata", mhttpd_audio_loadeddata);
3656 //audio.addEventListener("canplay", mhttpd_audio_canplay);
3657 //audio.addEventListener("canplaythrough", mhttpd_audio_canplaythrough);
3658 //audio.addEventListener("ended", mhttpd_audio_ended);
3659 //audio.addEventListener("paused", mhttpd_audio_paused);
3661 let promise = audio.play();
3663 promise.then(function(e) {
3664 //console.log(Date() + ": mhttpd_alarm_play: promise fulfilled, counter " + audio.counter);
3665 }).catch(function(e) {
3666 //count_audio_done++;
3667 //console.log(Date() + ": mhttpd_alarm_play: audio.play() exception: " + e + ", created: " + count_audio_created + ", done: " + count_audio_done);
3668 console.log(Date() + ": mhttpd_alarm_play: Cannot play alarm sound: audio.play() exception: " + e + ", counter " + audio.counter);
3669 // NB: must clear the URL of the sound file, otherwise, the sound file is still loaded (but not played)
3670 // the loading of sound files is observed to be delayed in inactive tabs
3671 // resulting in many (100-1000) pending loads getting queued, observed
3672 // to all of them attempt to run (load the sound file) in parallel,
3673 // consuming memory (100-300 Mbytes) and CPU (10-15% CPU per inactive tab).
3675 // NB: setting audio to pause() does not seem to do anything.
3683function mhttpd_alarm_play() {
3684 if (mhttpdConfig().alarmSound && mhttpdConfig().alarmSoundFile) {
3685 let now = new Date() / 1000;
3686 let last = mhttpdConfig().var.lastAlarm;
3687 let next = last + parseFloat(mhttpdConfig().alarmRepeat);
3688 let wait = next - now;
3689 let do_play = (now > next);
3690 //console.log("mhttpd_alarm_play: now: " + now + ", next: " + next + ", last: " + last + ", wait: " + wait + ", play: " + do_play);
3692 mhttpdConfigSet("var.lastAlarm", now);
3693 mhttpd_alarm_play_now();
3698function mhttpd_speak_now(text) {
3699 let u = new SpeechSynthesisUtterance(text);
3700 u.voice = speechSynthesis.getVoices().filter(function (voice) {
3701 return voice.name === mhttpdConfig().speakVoice;
3703 u.volume = mhttpdConfig().speakVolume;
3704 speechSynthesis.speak(u);
3707function mhttpd_speak(time, text) {
3709 if (!('speechSynthesis' in window))
3712 if (mhttpdConfig().speakChat) {
3713 if (time > mhttpdConfig().var.lastSpeak) {
3714 mhttpdConfigSet("var.lastSpeak", time);
3715 mhttpd_speak_now(text);
3724 * js-indent-level: 3
3725 * indent-tabs-mode: nil