75 Commits

Author SHA1 Message Date
a6ec455563 select_experiment: move links to instruments to top 2025-05-23 13:50:21 +02:00
be404b625b main js: fix clientTags 2025-05-23 13:05:17 +02:00
191f0aed80 logfile name depending on port 2025-05-23 13:04:37 +02:00
b24384f387 improve further select_experiments: add time range in link 2025-05-21 15:37:19 +02:00
e136b66732 no extra headline for central server 2025-05-21 13:14:26 +02:00
822e3ab6a2 big fix: treat history_only=None properly 2025-05-21 13:08:06 +02:00
b1ffe99a5d remove grey background on select_experiments 2025-05-21 12:11:16 +02:00
7d4607e947 work on select_experiment
- on currently running exp. make two links, to instrument
  or history only
- create dummy secop client when hideRightPart is used in order
  to avoid waiting impossible connection
2025-05-21 11:35:25 +02:00
55dd7a3777 fix bug not changing host 2025-05-20 16:59:25 +02:00
fd37ee0dfc cosmetics on home 2025-05-20 16:55:10 +02:00
90e8aa5df0 select_experiment: use links to instruments 2025-05-20 16:51:01 +02:00
6df42b9541 continue with graphics when secop connection fails 2025-05-20 16:51:01 +02:00
2984c051e9 use single instrument name instead of 'n_a' 2025-05-20 16:04:38 +02:00
46fca20f09 add links to servers on intruments 2025-05-20 15:10:19 +02:00
2ed1e3c292 instrument=main: for multiple instruments (on linse-c) 2025-05-20 15:10:19 +02:00
6390d37ab4 add usual locations of sehistory and frappy to sys.path 2025-05-20 10:30:00 +02:00
925bdfc472 improve command handling 2025-05-13 11:11:19 +02:00
3b438d68b2 fix bug in select_experiment page 2025-05-13 11:11:19 +02:00
a94cc98c42 remove obsolete stuff + small adjustments 2025-05-13 11:11:19 +02:00
2cd78ae1e9 rename files with spaces in doc 2025-05-13 11:11:19 +02:00
a4fda418b2 fix chart config parameters
- add SEA dil pressures
- read config each time when it is used
2025-05-13 11:11:19 +02:00
179db4c0a3 remove some console.log for debug 2025-05-13 11:10:35 +02:00
5c70017238 fix simple command (e.g. stop) 2025-05-13 11:10:27 +02:00
268ebc7e93 lazyPermission: allow to configure starting with writePermission=true 2025-05-13 11:10:14 +02:00
f66b071813 Merge commit '62da014d4041e' 2025-05-13 11:09:38 +02:00
d6a69ba05e Prepare radio-button-group 2025-05-13 11:09:04 +02:00
f883c913ed Merge branch 'master' of gitlab.psi.ch:samenv/seweb 2025-05-02 11:04:07 +02:00
a229626bdf change value of input element only when opening the edit
updates do not need to update them
(not yet done for enum and checkbox)
2025-05-02 11:02:22 +02:00
0a75a0aa37 Merge branch 'daniel' - 2025-05-02 2025-05-02 11:01:47 +02:00
d2ce97b2ab Fixed some layout issues fpr modules and parameters 2025-05-02 11:01:31 +02:00
06bdbbb02e change value of input element only when opening the edit
updates do not need to update them
(not yet done for enum and checkbox)
2025-05-02 10:56:53 +02:00
d6479a7ece pushbutton changed to link in left column 2025-05-02 10:55:53 +02:00
a8e14eb797 okay-button for input 2025-05-02 10:52:32 +02:00
b704440f36 make write permission check case insensitive 2025-05-02 10:45:48 +02:00
10387261c0 back to (modified) old icon theme 2025-05-02 10:45:15 +02:00
9f2bfd668a Changed some icons 2025-05-02 10:45:09 +02:00
70b40646ef modules-Block: prepared different input options, grid-element: panel-background added -> icon always visible 2025-05-02 10:44:46 +02:00
43624b4222 send instrument='n_a' if not available
instead of null

+ remove another debugging print statement
2025-05-02 10:43:34 +02:00
e51942c97f make sure instrument and device are availabel separately
- added comment to the alert to be modified
  ("You changed a field without pressing the return key.")
2025-05-02 10:43:34 +02:00
ae7ed7bdd8 get a faster response from a change
in the previous implementation, the row-waiting-for-answer
class was removed only on the next poll. this should happen
more quickly.

+ remove some print statements
2025-05-02 10:43:34 +02:00
202095d539 change row-waiting-for-answer color to a light yellow
orangered is too aggressive
2025-05-02 10:42:40 +02:00
cf0a979cc9 add nv.speed to generic.ini 2025-05-02 10:42:40 +02:00
8d38dc31f2 Changed some icons, modules block: different input elements (not tested), write permission promt 2025-05-02 10:42:40 +02:00
9bacb41be8 Global write permission | lock button
+ lock icon added: click -> toggle global write permission
2025-05-02 10:42:40 +02:00
8119f221bd input element display:block instead of float -> displayed in the same row 2025-05-02 10:42:40 +02:00
b9a1e7db99 Changed updateValue... 2025-05-02 10:42:40 +02:00
2fda3164e6 use type=rdonly for modules without target, but with a value 2025-05-02 10:42:40 +02:00
0a6ff13ee6 treat different types for module block 2025-05-02 10:42:40 +02:00
9780ab7097 Changed some icons... 2025-05-02 10:42:08 +02:00
9b7261261f fix do command 2025-05-02 10:42:08 +02:00
f6aff481e2 Fixed some display problems for module and parameter block 2025-05-02 10:42:08 +02:00
e6e69c8f5c Edit button for module block 2025-05-02 10:42:08 +02:00
df582a2f23 console not shown at start, infobox for touch device 2025-05-02 10:41:55 +02:00
8f7406c31b fixes for select_instrument 2025-05-02 10:41:55 +02:00
09bf402bbb implement SECoP commands on the server side 2025-05-02 10:41:55 +02:00
6e20ed0f8f Some bugfixes for input elements 2025-05-02 10:39:18 +02:00
960e95c447 Diverse Anpassungen besonders bei SEAWebClientGroup 2025-05-02 10:39:18 +02:00
27f60e1187 report status and target in update_main
+ add SECoP commands [WIP]
+ fix case 4 (error icon) in updateStatus (SEAWebClientCommunication.js)
2025-05-02 10:39:18 +02:00
5c1c94bffc Änderungen für den Modul- und denParameterblock 2025-05-02 10:39:18 +02:00
5d10b6d48d console, modules 2025-05-02 10:39:18 +02:00
d1ea9225dc status-icon with statuscode, use entire row as link to parameters 2025-05-02 10:39:18 +02:00
639949f24b change status update slightly
- formatted is like "BUSY, moving"
- value as with other tuples
- statuscode 0..4
2025-04-15 17:52:22 +02:00
555aca9ed0 add statuscode to update for status
- value in a status update now contains the text only
2025-04-15 17:37:29 +02:00
38b2dbcf93 added component info (from SECoP description)
+ add target update for modules block
2025-04-15 17:37:29 +02:00
33c9896bb1 Another icon 2025-04-15 17:37:29 +02:00
da17309e78 some more icons 2025-04-15 17:37:29 +02:00
9b3f89ddaa Some missing icons 2025-04-15 17:37:29 +02:00
3d37b10d61 Some new icons with logic (not completed yet) 2025-04-15 17:37:29 +02:00
8d668f7de6 Some esponsivity bugfixes 2025-04-15 17:37:29 +02:00
85fdaa445f server: remove unnecessary nesting in SecopInteractor.get_components 2025-04-15 17:37:29 +02:00
e132b43263 fix missing initialization of node_map in SecopInteractor 2025-04-15 17:37:28 +02:00
56f85d9a39 small fixes
- variable homeButton was removed in SEAWebClientMain.js
- add webserver.log to .gitignore
2025-04-15 17:37:28 +02:00
bc9c1361e5 followup change: fix dummy webserver
the dummy server was no longer working after server rework
2025-04-15 17:37:28 +02:00
11a475149a Restored some HTML-Debugging-tools 2025-04-15 17:37:28 +02:00
9f5ae58b9b rework of the server side
main change: the same server may be used for several instruments

- client classes are 'Interactors' dealing with the parameters etc.
- methods from history are added to the clients
+ improvements on the js client side
2025-03-19 08:37:58 +01:00
16 changed files with 202 additions and 1121 deletions

View File

@ -93,9 +93,9 @@ class Client(HandlerBase):
result = node.handle_command(command)
if result is not None:
break
if isinstance(result, str):
if isinstance(result, dict):
return dict(type='accept-command', result=result)
return dict(type='accept-command') # TODO: how to handle result is None?
return dict(type='accept-command')
def info(self):
return ["na"]

View File

@ -1,4 +1,5 @@
from configparser import ConfigParser
import logging
class ChartConfig:
@ -16,16 +17,15 @@ class ChartConfig:
Parameters :
path (str) : the path to the configuration file
"""
self.errors = {}
self.variables = {}
cfgp = ConfigParser(interpolation=None)
cfgp.optionxform = str
cfgp.read(path)
section = cfgp["chart"]
try:
section = cfgp["chart"]
except KeyError:
return
for key, raw_value in section.items():
if len(key.split('.')) > 1:
self.errors[key] = f'illegal key: {key}'
continue
arguments = raw_value.split(",")
keyword_mode = False
config = {'cat': '*'}
@ -36,25 +36,11 @@ class ChartConfig:
config[argname] = argvalue
else:
if keyword_mode:
self.errors[key] = f"positional arg after keywd arg: {key}={raw_value!r}"
logging.error('positional arg after keywd arg: %s=%r', key, raw_value)
else:
try:
if argvalue:
config[self.KEYS[i]] = argvalue
except Exception as e:
self.errors[key] = f"{e!r} in {key}={raw_value}"
logging.error('%r in %s=%r', e, key, raw_value)
self.variables[key] = config
def get_variable_parameter_config(self, key):
"""
Gets the configuration of the given key in the configuration file (chart section).
Parameters :
key (str) : the key to look for in the chart section (<variable>[.<param>])
Returns :
{"cat":(str), "color":(str), "unit":(str)} | None : a dictionnary representing the different options for the given key in the chart section.
The different options are in this dict if they are found in the chart section for the given key. Returns None if the key is not in the chart section,
or if there is a syntax problem for the given key.
"""
return self.variables.get(key)

View File

@ -48,7 +48,7 @@
}
.row-waiting-for-answer {
background-color: orangered;
background-color: LightGoldenrodYellow;
}
/* ------------------------------ icon-modules ------------------------------ */

View File

@ -215,18 +215,10 @@ function handleUpdate(message, src) {
function updateTarget(component) {
let matches = document.getElementsByName(component.name);
let elem = matches[0]; // Should be the only match
elem.value = component.value;
// elem.value = component.value;
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
let oldValue = ('oldValue' in elem) ? elem.oldValue : elem.value;
if (component.value != elem.value && parseFloat(component.value) != parseFloat(elem.value) && component.value != oldValue) {
if (elem == document.activeElement || oldValue != elem.value) {
row.classList.add('row-waiting-for-answer');
} else {
elem.value = component.value;
}
}
elem.actualValue = component.value;
if(elem.__ctype__ == 'input') {
resizeTextfield(elem);
@ -277,13 +269,12 @@ function updateValue(component) {
for (var j = 0; j < matches.length; j++) {
let elem = matches[j];
let type = elem.__ctype__; // -> Show Dom-Properties
if (type == "rdonly" || type == "none") {
if (type == "rdonly") {
let text = htmlEscape(component.formatted);
if (text) {
elem.innerHTML = text;
}
}
else if (type == "input") {
} else if (type == "input") {
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
elem.actualValue = component.value;
@ -306,10 +297,10 @@ function updateValue(component) {
// elem.value = j + 1;
// }
// }
elem.value = component.value;
console.log('update pushbutton');
console.log('component.name:', component.name);
console.log('element', elem);
} else if (type == "none") {
// pushbutton (e.g. stop command)
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
}
}
}
@ -468,6 +459,11 @@ function successHandler(s, message) {
// Response to a "updategraph"-server-request.
case "accept-graph":
break;
case "accept-command":
if (message.result) {
updateValue(message.result);
}
break;
case "error":
console.log("%cError-Message received!", "color:white;background:red");
console.log(message);

View File

@ -342,7 +342,7 @@ function loadGraphicsMenu(panel){
menuGraphicsPopup.addEntry(aboutTopRightHandCornerCrossHelpEntry);
// let graphicsMenuControl = new Control("res/menu_white_wide.png", "res/menu_white_wide.png", "Menu", () => {
let graphicsMenuControl = new Control("res/icon_menu_graphics.png", "icon_menu_graphics.png.png", "Menu", () => {
let graphicsMenuControl = new Control("res/icon_menu_graphics.png", "res/icon_menu_graphics.png", "Menu", () => {
datesPopup.hide();
exportPopup.hide();
curvesSettingsPopup.hide();

View File

@ -1,4 +1,4 @@
var writePermission = true;
var writePermission = false;
var showParams = false;
var showConsole = false;
var prompt = false // True while a prompt is opened.
@ -223,7 +223,11 @@ function createValue (component) {
value.classList.add('col-right-value-with-write-permission');
}
value.setAttribute('name', component.name);
value.__ctype__ = 'rdonly';
if (component.type == 'pushbutton') {
value.__ctype__ = 'none';
} else {
value.__ctype__ = 'rdonly';
}
return value;
}
@ -501,369 +505,3 @@ function appendToGridElement(s, title, type, content) {
gridelements[s].innerHTML = "";
gridelements[s].appendChild(gridContainer);
}
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
/* ---------------------------------------------------------------------------------- */
// obsolete...
// function createPushbutton_row(component) {
// // Creates row-element containing a push button
// var name = component.name;
// var command = component.command;
// var left = createTitle(component);
// left.id = component.name;
// left.name = component.title;
// var right = createInputElement(component);
// right.classList.add("push-button");
// row = appendToContent(left, right);
// right.onclick = function () {
// if (writePermission) {
// var row = left.parentNode;
// right.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command);
// } else {
// prompt = true;
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command);
// prompt = false;
// }, function () {
// // User decided to cancel
// prompt = false;
// });
// }
// }
// row.classList.add("row");
// return row;
// }
// function create_input_row(component) {
// // Creates row-element containing input-item.
// var name = component.name;
// var command = component.command;
// var left = createTitle(component);
// var input = createInputElement(component, 'input', 'input-text');
// input.type = "text";
// input.style.width = "100px";
// input.addEventListener("focus", function(evt) {
// let elm = evt.target;
// setTimeout(function(){elm.setSelectionRange(0, elm.value.length);},0);
// });
// input.onkeydown = function (e) {
// if (e.which === 27 || e.key == "Escape") {
// // User decided to cancel
// input.value = intput.oldValue;
// resizeTextfield(input);
// var row = left.parentNode;
// row.style.backgroundColor = "white";
// }
// }
// input.onfocus = function () {
// input.oldValue = input.value;
// if (isTouchDevice)
// setTimeout(function () {
// posTextfield(s, left);
// }, 1);
// }
// input.onblur = function () {
// if (prompt) {
// return false;
// }
// var row = left.parentNode;
// var value = input.value;
// let oldValue = 'oldValue' in input ? input.oldValue : value;
// if (!('actualValue' in input)) input.actualValue = oldValue;
// actualValue = input.actualValue;
// if (value == actualValue || value == oldValue ||
// parseFloat(value) == parseFloat(actualValue) || parseFloat(value) == parseFloat(oldValue)) {
// input.value = actualValue;
// // nothing to do.
// row.style.backgroundColor = "white";
// return false;
// }
// // User changed value and moved focus to other object.
// alertify.confirm("", "You changed a field without pressing the return key.<br>"
// + "Hint: press ESC for leaving a field unchanged.<b>"
// + "You are connected with <b>" + clientTitle + "</b>.<br>"
// + "Are you sure you want to change the value of<br><b>"
// + name + "</b> from <b>" + actualValue
// + "</b> to <b>" + value + "</b>?", function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command + " " + value);
// resizeTextfield(input);
// prompt = false;
// }, function () {
// // User decided to cancel
// input.value = input.actualValue;
// resizeTextfield(input);
// row.style.backgroundColor = "white";
// prompt = false;
// });
// }
// var form = document.createElement('form');
// form.onsubmit = function (e) {
// e.preventDefault();
// if (writePermission) {
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// input.actualValue = input.value;
// sendCommand(s, name + " " + input.value);
// input.blur();
// } else {
// var value = input.value
// prompt = true;
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// input.actualValue = value;
// // Request for command
// sendCommand(s, command + " " + value);
// resizeTextfield(input);
// prompt = false;
// }, function () {
// // User decided to cancel
// input.value = input.oldValue;
// resizeTextfield(input);
// prompt = false;
// });
// }
// };
// form.appendChild(input);
// var right = createInputElement(component);
// right.appendChild(form);
// return appendToContent(left, right);
// function posTextfield(s, left) {
// if (debug_group_daniel) {
// console.log("%cfunction: posTextfield", "color:white;background:salmon");
// }
// // var content = swiper[s].slides[swiper[s].activeIndex].childNodes[1];
// // var row = left.parentNode;
// // content.scrollTop = row.offsetTop - 30;
// // ---------------------> Not working anymore since swiper was removed!!!
// }
// }
// function createCheckbox_row(component) {
// // Creates row-element containing checkbox-item
// var command = component.command;
// var left = createTitle(component);
// var input = createInputElement(component, 'input', 'parameter-checkbox');
// input.type = "checkbox";
// input.onkeyup = function (e) {
// if (e.keyCode === 32) {
// handleCheckbox();
// }
// }
// var label = document.createElement('label');
// label.for = input;
// label.classList.add("parameter-label");
// label.onclick = function () {
// handleCheckbox();
// }
// function handleCheckbox() {
// if (writePermission) {
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// if (input.checked) {
// var value = "0";
// input.checked = false;
// } else {
// var value = "1";
// input.checked = true;
// }
// // Request for command
// sendCommand(s, command + " " + value);
// } else {
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// if (input.checked) {
// var value = "0";
// input.checked = false;
// } else {
// var value = "1";
// input.checked = true;
// }
// // Request for command
// sendCommand(s, command + " " + value);
// }, function () {
// // User decided to cancel
// });
// }
// };
// var right = document.createElement('span');
// right.classList.add("col-right");
// right.appendChild(input);
// right.appendChild(label);
// return appendToContent(left, right);
// }
// function createEnum_row(component) {
// // Creates row-element containing dropdown-selection.
// var name = component.name;
// var command = component.command;
// var buttons = component.enum_names;
// var left = createTitle(component);
// var select = createInputElement(component, 'select', 'select-params');
// select.onfocus = function () {
// select.oldIndex = select.selectedIndex;
// }
// select.oninput = function () {
// if (writePermission && component.title != "device config") {
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command + " " + this.value);
// } else {
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command + " " + select.value);
// }, function () {
// // User decided to cancel
// select.value = select.options[select.oldIndex].value
// });
// }
// };
// for (var i = 0; i < buttons.length; i++) {
// var option = document.createElement('option');
// option.type = "enum";
// option.classList.add("option-params");
// option.value = buttons[i].value;
// option.appendChild(document.createTextNode(buttons[i].title));
// select.add(option);
// }
// select.style.display = "none";
// var right = document.createElement('span');
// right.classList.add("col-right");
// right.appendChild(select);
// return appendToContent(left, right);
// }
// ...obsolete
/* ---------------------------------------------------------------------------------- */
// The following two functions seem unreachable
/* ########## Is this function used? How? Where? ########## */
// function create_group_row(component) {
// // Creates row-element containing link.
// var title = component.title;
// var row = document.createElement('row');
// row.id = component.name;
// row.name = title;
// row.classList.add("row");
// row.tabIndex = "0";
// console.log('createGroupRow');
// row.onclick = function () {
// var slideNames = getSlideNames();
// slideNames[0] = component.name;
// document.title = "SEA "+ clientTitle + " " + slideNames.join(" ");
// history.pushState({func: "gotoGroups", funarg: slideNames.join("%20")}, document.title, "#" + slideNames.join("%20"));
// getGroup(s, component.name);
// }
// if (title === "console" || title === "device config") {
// row.classList.add("row");
// row.innerHTML = "console";
// }
// row.innerHTML = title;
// return row;
// }
/* ########## Is this function used? How? Where? ########## */
// function create_rdonly_row(component) {
// // Creates row-element containing link AND read-only-item.
// var link = component.link;
// if (!link) // simple rdonly
// return appendToContent(createTitle(component),
// createInputElement(component));
// // with link
// var left = document.createElement('a');
// left.innerHTML = component.title;
// console.log('ccc')
// left.id = component.name;
// row = appendToContent(left, createInputElement(component));
// row.onclick = function () {
// this.style.backgroundColor = "orangered";
// left.click();
// }
// if (link.charAt(0) == ':') {
// left.href = "http://" + location.hostname + link + "/";
// } else {
// left.href = link;
// }
// row.classList.add("row");
// return row;
// }

View File

@ -75,15 +75,18 @@ new Settings()
.treat("stream", "stream", 0, "")
.treat("instrument", "instrument", 0, "")
.treat("timerange", "time", 0, "-1800,0")
.treat("lazyPermission", "wr", to_bool, true);
let args = '';
if (window.instrument) {
window.clientTags = "&instrument=" + window.instrument;
args += "&instrument=" + window.instrument;
} else {
let args = '';
if (window.stream) { args += "&stream=" + window.stream; }
if (window.device) { args += "&device=" + window.device; }
window.clientTags = args;
}
if (window.hideRightPart) { args += "&history_only=1"; }
window.clientTags = args;
// console.log('TAGS', window.clientTags);
@ -183,35 +186,24 @@ window.onload = function() {
adjustGrid();
}
icon_lock_container.onclick = function(){
function changeWritePermission(flag) {
let array_icon_edit = document.getElementsByClassName('icon-edit');
let array_pushbutton = document.getElementsByClassName('push-button');
let array_col_right_value = document.getElementsByClassName('col-right-value');
if (writePermission == false) {
alertify.prompt( "WRITE PERMISSION", "Current device: <b>"+ window.device + "</b><p>Please confirm the instrument:", ""
, function(evt, value) {
// User decided to proceed
if (window.instrument.toUpperCase() == value.toUpperCase()) {
writePermission = true;
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_open.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.remove('icon-edit-hidden');
}
for(i = 0; i < array_pushbutton.length; i++) {
array_pushbutton[i].classList.add('push-button-active');
}
for (let i = 0; i < array_col_right_value.length; i++) {
array_col_right_value[i].classList.add('col-right-value-with-write-permission');
}
}
writePermission = flag;
if (writePermission) {
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_open.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.remove('icon-edit-hidden');
}
for(i = 0; i < array_pushbutton.length; i++) {
array_pushbutton[i].classList.add('push-button-active');
}
for (let i = 0; i < array_col_right_value.length; i++) {
array_col_right_value[i].classList.add('col-right-value-with-write-permission');
}
, function() {
// User decided to cancel
prompt = false;
});
} else {
writePermission = false;
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_closed.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.add('icon-edit-hidden');
@ -225,6 +217,33 @@ window.onload = function() {
}
}
if (window.lazyPermission) {
changeWritePermission(true);
}
icon_lock_container.onclick = function(){
if (writePermission == false) {
if (window.lazyPermission) {
changeWritePermission(true);
} else {
alertify.prompt( "WRITE PERMISSION", "Current device: <b>"+ window.device + "</b><p>Please confirm the instrument:", ""
, function(evt, value) {
// User decided to proceed
if (window.instrument.toUpperCase() == value.toUpperCase()) {
changeWritePermission(true);
}
}
, function() {
// User decided to cancel
prompt = false;
});
}
} else {
changeWritePermission(false);
}
}
var homeButton = document.getElementById("home-icon");
homeButton.onclick = function () {

View File

@ -1 +1,9 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{"name":"",
"short_name":"",
"icons":[
{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},
{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],
"theme_color":"#ffffff",
"background_color":"#ffffff",
"display":"fullscreen"}

View File

@ -19,6 +19,13 @@ T_sorb=unit:K,color:dark_violet
T_sorb.target=-
T_still=unit:K,color:orange
dil=-
dil.G1=unit:mbar
dil.G2=unit:mbar
dil.G3=unit:mbar
dil.P1=unit:mbar
dil.P2=unit:mbar
dil.v6pos=unit:%
dil.V12A=unit:%
lev=unit:%,color:brown
lev.n2=unit:%,color:black
hefill=-

View File

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -1,624 +0,0 @@
from influxdb_client import InfluxDBClient
from configparser import ConfigParser
import ast
from datetime import datetime
from pandas import DataFrame as df, merge_ordered
from numpy import NaN
MEASURMENT_PREFIX = "nicos/se_"
BUCKET_PREFIX = "nicos-cache-"
class InfluxDB:
"""
Class used to handle the connection with the InfluxDB instance
Attributes :
_client (InfluxDBClient) : the InfluxDB client
"""
def __init__(self):
config = ConfigParser()
config.optionxform = str
config.read("./config/influx.ini")
self._client = InfluxDBClient(url=config["INFLUX"]["url"], token=config["INFLUX"]["token"],
org=config["INFLUX"]["org"])
def disconnet(self):
"""
Disconnects from the InfluxDB instance
"""
self._client.close()
def query(self, query_str):
"""
Executes the query on the InfluxDB instance
Parameters :
query_str (string) : the Flux query to execute
Returns :
TableList : an InfluxDB list of the tables returned by the query
"""
return self._client.query_api().query(query_str)
def query_data_frame(self, query_str):
"""
Executes the query on the InfluxDB instance and gets the response as a pandas DataFrame
Parameters :
query_str (string) : the Flux query to execute
Returns :
DataFrame : the query response as a DataFrame
"""
return self._client.query_api().query_data_frame(query_str)
class PrettyFloat(float):
"""saves bandwidth when converting to JSON
a lot of numbers originally have a fixed (low) number of decimal digits
as the binary representation is not exact, it might happen, that a
lot of superfluous digits are transmitted:
str(1/10*3) == '0.30000000000000004'
str(PrettyFloat(1/10*3)) == '0.3'
"""
def __repr__(self):
return '%.15g' % self
class InfluxDataGetter:
"""
Class used to get data from InfluxDB.
Attributes :
_bucket (str) : the name of the InfluxDB bucket to query (used for all queries)
_db (InfluxDB) : the InfluxDB instance of the database to query
"""
def __init__(self, db, instrument_name):
"""
Parameters :
db (InfluxDB) : the InfluxDB instance of the database to query
instrument_name (str) : the name of the instrument from which the data will be got
"""
self._bucket = BUCKET_PREFIX + instrument_name
self._db = db
# ----- PUBLIC METHODS
def get_curves_in_timerange(self, variables, time, interval = None):
"""
Gets the curves for the given variables within a timerange.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the curves for
time ([int]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int|None) : the interval (resolution) of the values to get (in nanoseconds)
Returns :
{(str):[[(int), (float)]]} : a dictionnary of curves. The key is the name of the influx variable, and the value is an array of pairs (also arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y).
"""
res = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
curve = self._get_curve(variable_name_for_query, parameter, time, interval)
res[variable] = curve
return res
def poll_last_values(self, variables, lastvalues, end_time):
"""
Polls the lastest values for the given variables since their last known point to end_time.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the last known values for
lastvalues ({(str):((float), (float))}) : a dictionnary of tuples, first value being the floating Unix timestamp in seconds (precision = ms) of the last known value for the curve, and the second value being the associated value, indexed by the curve name
end_time (int) : the Unix timestamp in seconds of the last point in time to include the values in
Returns :
{(str):[[(int), (float)]]} : a dictionnary of points. The key is the name of the influx variable, and the value is an array of pairs (also array), the first value being the Unix timestamp in second (x), the seconds being the value (y).
"""
res = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
start_time = int(lastvalues[variable][0]) if variable in lastvalues.keys() else None #if the call to poll_last_values is more recent than getgraph, we trigger only one value, which is the last one available from 0 to end_time
points = []
if start_time == None or start_time < end_time: # start_time might be more recent than end_time if getgraph is called between graphpoll and pol_last_values (influxgraph.py)
points = self._get_last_values(variable_name_for_query,parameter,start_time, end_time)
if len(points) > 0 :
res[variable] = points
return res
def get_device_name(self, time):
"""
Gets the device name available at time with stick and addons
Parameters :
time (int) : the Unix timestamp in seconds of the time we want the device name
Returns :
str : the device name
"""
components = self._get_device_name_components(time)
return "/".join(components)
def get_curves_data_frame(self, variables, times, interval, variables_name_label_map=None):
"""
Gets the curves for the given variables within a timerange times, as a pandas dataframe.
All curves are on a single common time axis.
The first column is called "relative", and consists of floating seconds, relative to the beginning of the query.
The "timestamp" column (absolute floating UNIX timestamps in seconds, precise to the nanosecond) is the last one.
If a curve does not have a point at a given point, the last known value for this curve is used.
If a curve gets expired, it is filled with NaN value for the corresponding expired time windows.
The first value (for each variable) is the last known value before the time interval.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the curves for.
times ([int]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int) : the interval (resolution) of the values to get (in seconds). Allows data binning.
variables_name_label_map ({(str):(str)} | None) : a dictionnary containing curve labels, indexed by INFLUX names. The corresponding label will be used in the TSV header for each variable if found, else the influx name.
Returns :
pandas.DataFrame : the curves in a single pandas DataFrame
"""
variables_info = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
# we need to rename the "_time" column to simply "time" in case we want binning because of the comparison done later in the "binned points with same timestamp" process.
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {times[0]}, stop: {times[1] + 1})
|> filter(fn : (r) => r._measurement == "{variable_name_for_query}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
{f'|> aggregateWindow(every: duration(v: {self._seconds_to_nanoseconds(interval)}), fn: last, createEmpty:false, timeDst:"binning_time")' if interval != 'None' else ''}
|> map(fn: (r) => ({{r with relative: ( float(v: uint(v: {"r.binning_time" if interval != "None" else "r._time"}) - uint(v:{self._seconds_to_nanoseconds(times[0])})) / 1000000000.0 )}}))
|> map(fn: (r) => ({{r with timestamp: float(v: uint(v: {"r.binning_time" if interval != "None" else "r._time"})) / 1000000000.0}}))
{'|> rename(columns: {_time:"time"})' if interval != 'None' else ''}
|> drop(columns:["_start", "_stop", "_field"])
|> pivot(rowKey:["relative", "timestamp", "expired"{', "time"' if interval != "None" else ''}], columnKey: ["_measurement"], valueColumn: "_value")
"""
data_frame = self._db.query_data_frame(query)
# If there is a binning asked, there can be two points with the same timestamp/relative value, because points with expired=True and expired=False can be found in the same interval, leading to two rows, one with last True, and one with last False.
# For each identified couple, we look at the real timestamp of the points used to feed this interval. We then keep the value which is the more recent in this interval.
if interval != "None" and not data_frame.empty:
# we first identify the expired points
expired_rows = data_frame.loc[data_frame["expired"] == "True"]
# we use itertuples to preserve the pandas dtypes, so comparisons can be done. "tuple" is a Python named tuple
for expired_point_tuple in expired_rows.itertuples():
# Then, we identify if there is a valid point with the same relative time as the current expired point
corresponding_valid_point = data_frame.loc[(data_frame["expired"] == "False") & (data_frame["relative"] == expired_point_tuple.relative)]
# we make sure that there is only one corresponding valid point, even if in theory, there will never be more than one corresponding valid point
if not corresponding_valid_point.empty and len(corresponding_valid_point.index) == 1:
# if we did not rename "_time" to "time" sooner in the query, "_time" would have been renamed to a positionnal name (because it starts with a _, see itertuples() doc), making confusion while reading the code
if corresponding_valid_point.iloc[0]["time"] > expired_point_tuple.time:
data_frame.drop(expired_point_tuple.Index, inplace=True)
else:
data_frame.drop(corresponding_valid_point.index, inplace=True)
# we do not need the "time" column anymore
data_frame.drop(["time"], axis=1, inplace=True)
# Needed for last known value
query_last_known = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {times[0] + 1})
|> filter(fn : (r) => r._measurement == "{variable_name_for_query}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
|> last()
|> map(fn: (r) => ({{r with relative: 0.0}}))
|> map(fn: (r) => ({{r with timestamp: float(v: uint(v: r._time)) / 1000000000.0}}))
|> drop(columns:["_start", "_stop", "_field"])
|> pivot(rowKey:["relative", "timestamp", "expired"], columnKey: ["_measurement"], valueColumn: "_value")
"""
data_frame_last_known = self._db.query_data_frame(query_last_known)
row_to_insert = None
for index, row in data_frame_last_known.iterrows():
try: #needed because row_to_insert == None is not possible
if row_to_insert.empty or row["timestamp"] > row_to_insert["timestamp"]:
row_to_insert = row
except:
row_to_insert = row
try: #row_to_insert might be None
if not row_to_insert.empty :
row_to_insert["timestamp"] = float(times[0])
if data_frame.empty:
data_frame = row_to_insert.to_frame().T
else:
data_frame.loc[-1] = row_to_insert
except:
pass
if data_frame.empty:
continue
variable_df_column_name = variables_name_label_map.get(variable, variable) if not variables_name_label_map == None else variable
data_frame.rename(columns={variable_name_for_query : variable_df_column_name}, inplace=True)
data_frame.drop(["result", "table"], axis=1, inplace=True)
data_frame.sort_values(by=["timestamp"], inplace=True)
data_frame.reset_index()
variables_info[variable_df_column_name] = {}
variables_info[variable_df_column_name]["expired_ranges"] = []
# Identify time windows for which the curve is expired
for index, row in data_frame.iterrows():
if row["expired"] == "True":
data_frame.loc[index, variable_df_column_name] = NaN
variables_info[variable_df_column_name]["expired_ranges"].append([row["timestamp"]])
elif row["expired"] == "False":
if len(variables_info[variable_df_column_name]["expired_ranges"]) > 0 and len(variables_info[variable_df_column_name]["expired_ranges"][-1]) == 1:
variables_info[variable_df_column_name]["expired_ranges"][-1].append(row["timestamp"])
data_frame.reset_index()
data_frame.drop(["expired"], axis=1, inplace=True)
variables_info[variable_df_column_name]["df"] = data_frame
res = None
non_empty_variables = list(variables_info.keys())
# Merge single curve dataframes to a global one
if len(non_empty_variables) == 0:
return df()
elif len(non_empty_variables) == 1:
res = variables_info[non_empty_variables[0]]["df"]
else :
for i in range(0, len(non_empty_variables)):
if i == 1:
res = merge_ordered(variables_info[non_empty_variables[0]]["df"], variables_info[non_empty_variables[1]]["df"], on=["timestamp", "relative"], suffixes=(None, None))
elif i > 1:
res = merge_ordered(res, variables_info[non_empty_variables[i]]["df"], on=["timestamp", "relative"], suffixes=(None, None))
# Forward fill missing values, then set data points to NaN for those which are expired
if len(non_empty_variables) > 1:
res.ffill(inplace=True)
for variable, info in variables_info.items():
for expired_range in info["expired_ranges"]:
res.loc[(res["timestamp"] >= expired_range[0]) & ((res["timestamp"] < expired_range[1]) if len(expired_range) == 2 else True), variable] = NaN
# Change order of columns
cols = res.columns.tolist()
cols = [cols[0]] + cols[2:] + [cols[1]]
res = res[cols]
return res
# ----- PRIVATE METHODS
def _transform_secop_module_name_to_influx(self, secop_module_name):
"""
Transforms the name of the variable available in the setup_info dict into the Influx name.
Lowers the secop_module_name and adds "nicos/se_" as prefix
Parameters :
secop_module_name (str) : the secop module name of the variable in the setup_info dict.
Returns :
str : the transformed variable name that matches the Influx names reqauirements
"""
return MEASURMENT_PREFIX + secop_module_name.lower()
def _filter_params_with_config(self, available_variables, chart_config):
"""
Updates (cat, color, unit) the parameters of each variable according to the user_config object.
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
chart_config (ChartConfig) : the object holding a configuration file for the chart.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
key = variable["label"] if param_key == "value" else variable["label"]+"."+param_key
param_config = chart_config.get_variable_parameter_config(key)
if param_config != None :
for key, value in param_config.items():
variable["params"][param_key][key] = value
return available_variables
def _filter_params_with_user_config(self, available_variables, user_config):
"""
Updates (cat, color, unit) the parameters of each variable according to the user_config object.
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
user_config ({(str):{"cat":(str), "color":(str), "unit":(str)}}) : the Python dict representing the user configuration. The key is <secop_module.parameter>.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
key = variable["label"] if param_key == "value" else variable["label"]+"."+param_key
param_config = user_config[key] if key in user_config.keys() else None
if param_config != None :
for key, value in param_config.items():
variable["params"][param_key][key] = value
return available_variables
def _remove_variables_params_not_displayed(self, available_variables):
"""
Removes the parameters of each variable if their category is "-".
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
if variable["params"][param_key]["cat"] == "-":
del variable["params"][param_key]
return available_variables
def _remove_variables_params_wihout_param_float_and_split(self, available_variables, time):
"""
For each variable, removes the parameters if the Influx database does not contain <param>.float field, and split the parameters to the corresponding output format.
Parameters:
available_variables ([{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}]) : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnares with the category, the color and the unit, indexed by the name of the parameter.
time (int) : the unix timestamp in seconds of the point in time to get the variables at. Used to have an upper limit in the query.
Returns :
[{"name":(str), "label":(str), "cat":(str), "color":(str), "unit":(str)}] : an array of dictionnaries, each containing the name of the variable[.<param>],
the label to display in the Web GUI, its category, its color and its unit.
"""
res = []
for variable in available_variables:
query = f"""
import "influxdata/influxdb/schema"
schema.measurementFieldKeys(bucket: "{self._bucket}", measurement: "{variable["name"]}", start:0, stop: {time + 1})
|> yield(name: "res")
"""
records = self._db.query(query)[0].records
fields = [record.get_value() for record in records]
for param in variable["params"].keys():
if param+"_float" in fields:
curve = {
"name": variable["name"] if param == "value" else variable["name"]+"."+param,
"label": variable["label"] if param == "value" else variable["label"]+"."+param,
"cat": variable["params"][param]["cat"],
"color": variable["params"][param]["color"],
"unit": variable["params"][param]["unit"],
}
if param == 'target':
curve['period'] = 0
res.append(curve)
return res
def _group_variables_by_cat_unit(self, available_variables):
"""
Performs a group by cat if specified (different than "*"), or by unit instead for the available variables
Parameters :
available_variables ([{"name":(str), "label":(str), "cat":(str), "color":(str), "unit":(str)}]) : an array of dictionnaries, each containing the name of the variable[.<param>],
the label to display in the Web GUI, its category, its color and its unit.
Returns :
[{"tag":(str), "unit":(str), "curves":[{"name":(str), "label":(str), "color":(str)}]] : a list of dictionnaries, each one representing
a block of curves with their name, their label and their color to display, grouped by their tag, which is the unit or the category if given and not "unit".
"""
groups = {}
for available_variable in available_variables:
unit = available_variable.pop("unit")
key = available_variable.pop("cat").replace("*", unit)
if key not in groups.keys():
groups[key] = {"tag":key, "unit": unit, "curves":[]}
groups[key]["curves"].append(available_variable)
return list(groups.values())
def _get_curve(self, variable, parameter, time, interval=None):
"""
Gets the points (curve) within a timerange for the given variable and parameter.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
parameter (str) : the parameter of the variable to get the values from
time ([(int)]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int) : the interval (resolution) of the values to get (in milliseconds)
Returns :
[[(int), (float)]] : an array of pairs (also arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y)
"""
raw = []
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {time[0]}, stop: {time[1] + 1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
{"|> aggregateWindow(every: duration(v:"+str(self._milliseconds_to_nanoseconds(interval))+"), fn: last, createEmpty:false, timeDst:"+chr(34)+"binning_time"+chr(34)+")" if interval else ""}
|> keep(columns: ["_time","_value","expired"{", "+chr(34)+"binning_time"+chr(34) if interval else ""}])
|> yield(name: "res")
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record["binning_time"] if interval else record.get_time()), 3) # t is the real timestamp if no interval is given, or the binned timestamp if interval
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
point = [t, value, record["expired"]]
if interval: # t is the binning time, we need to add the real time of the point that was used in this interval
point.append(record.get_time())
raw.append(point)
if interval:
indexes_to_delete = []
expired_points = {i:point for i,point in enumerate(raw) if point[2] == "True"} #we need to keep track of the indexes of the expired point in the list
for expired_point_index, expired_point in expired_points.items():
for i, point in enumerate(raw):
if point[2] == "False" and expired_point[0] == point[0]: # if the current point is expired and has the same binning time as the current expired point
if point[3] > expired_point[3]: # comparison on the real timestamp used.
indexes_to_delete.insert(0, expired_point_index)
else:
indexes_to_delete.insert(0,i)
sorted(indexes_to_delete, reverse=True) #we have to make sure that the list is sorted in reverse to then delete at the given indexes
for index in indexes_to_delete:
del raw[index]
sorted_raw = sorted(raw, key=lambda pair: pair[0]) #expired=True and expired=False are in two Influx tables, so they need to be synchronized
res = []
for pair in sorted_raw:
if pair[2] == "True":
res.append([pair[0], pair[1]]) # So the user can know precisely when a curve is expired
res.append([pair[0], None]) # So chartJS will cut the curve from this point (which is expired)
else:
res.append([pair[0], pair[1]])
return self._insert_last_known_value(variable, parameter, res, time)
def _insert_last_known_value(self, variable, parameter, curve, time):
"""
Adds the last known value as the first point in the curve if the last known value is outside the viewing window, for the given variable and parameter.
The point is added only if it is not expired.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
parameter (str) : the parameter of the variable to get the values from
curve ([[(int), (float)]]) : an array of pairs (arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y)
time ([(int)]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
Returns :
[[(int), (float)]] : the curve of the parameter, updated with a potential new first point
"""
if len(curve) == 0 or curve[0][0] != time[0]:
query = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {time[0]+1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
|> last()
|> keep(columns: ["_time", "_value", "expired"])
|> yield(name: "res")
"""
tables = self._db.query(query)
pair_to_insert = []
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record.get_time()), 3)
value = None
if record["expired"] == "False":
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
if len(pair_to_insert) == 0 or t >= pair_to_insert[0]:
pair_to_insert = [t, value]
if len(pair_to_insert)==2 and pair_to_insert[1] != None:
curve.insert(0, [time[0], pair_to_insert[1]])
return curve
def _get_last_values(self, variable, parameter, start_time, end_time):
"""
Gets the lastest values for the given variable and parameter that are in [start_time, end_time].
The process is the same as _get_curve.
Parameters :
variable (str) : the name (Influx) of the variable we want the last value of.
parameter (str) : the parameter of the variable to get the values from
start_time (int|None) : the start of time range (Unix timestamp in seconds) to include the values in
end_time (int) : the end of time range (Unix timestamp in seconds) to include the values in
Returns :
[[(int), (float)]] : an array of points (also arrays). The first value is the Unix timestamp in second (x), the seconds is the value (y)
"""
raw = []
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {start_time if start_time != None else 0}, stop: {end_time+1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+ "_float"}")
{"|> last()" if start_time == None else ""}
|> keep(columns: ["_time","_value", "expired"])
|> yield(name: "res")
"""
# this loop might be simplified, but it has to be kept to catch the case when there is unavailable data
tables = self._db.query(query)
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record.get_time()), 3)
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
raw.append([t, value, record["expired"]])
sorted_raw = sorted(raw, key=lambda pair: pair[0])
res = []
for pair in sorted_raw:
if pair[2] == "True":
res.append([pair[0], pair[1]])
res.append([pair[0], None])
else:
res.append([pair[0], pair[1]])
return res
def _get_device_name_components(self, time):
"""
Gets the components of the device name, first in the main name, then stick, then addons.
Parameters :
time (int) : the Unix timestamp in seconds of the time we want the device name
Returns :
[str] : an array of string, each one being a component of the device name
"""
measurements = ["nicos/se_main", "nicos/se_stick", "nicos/se_addons"]
res = []
for measurement in measurements:
query = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {time + 1})
|> filter(fn: (r) => r._measurement == "{measurement}")
|> filter(fn: (r) => r._field == "value")
|> last()
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
name = ast.literal_eval(record.get_value())
if name != None and name != '':
res.append(ast.literal_eval(record.get_value()))
return res
def _seconds_to_nanoseconds(self, seconds):
return seconds * 1000000000
def _milliseconds_to_nanoseconds(self, milliseconds):
return milliseconds * 1000000

View File

@ -58,14 +58,11 @@ class InfluxGraph(HandlerBase):
self.server = server
self.db = server.db
# self.influx_data_getter = influx_data_getter
self.chart_configs = [ChartConfig("./config/generic.ini")]
self.chart_configs = ["./config/generic.ini"]
self.instrument = instrument
self.device_name = device_name
if instrument: # TODO: should it not be better to have inifiles per device?
try:
self.chart_configs.append(ChartConfig(f"./config/{instrument}.ini"))
except KeyError:
pass
self.chart_configs.append(f"./config/{instrument}.ini")
self.livemode = self.HISTORICAL
self.last_values = {} # dict <variable> of last known point (<time>, <value>)
self.last_time = {} # dict <stream> of last received time
@ -204,12 +201,11 @@ class InfluxGraph(HandlerBase):
config = {}
if chart_configs:
for chart_config in chart_configs:
for key, cfg in chart_config.variables.items():
for key, cfg in ChartConfig(chart_config).variables.items():
config.setdefault(key, {}).update(cfg)
if user_config:
for key, cfg in user_config.items():
config.setdefault(key, {}).update(cfg)
groups = {}
def add_to_groups(name, cat=None, unit='1', color='', label=None):

View File

@ -1,8 +1,10 @@
#!/usr/bin/env python
import sys
from os.path import expanduser
# look for sehistory and frappy at usual locations in home directory
sys.path.extend([expanduser('~'), expanduser('~/frappy')])
import argparse
import socket
from webserver import server
from base import Client
from influxgraph import InfluxGraph
@ -38,6 +40,5 @@ def parseArgv(argv):
args = parseArgv(sys.argv[1:])
instrument = socket.gethostname().split('.')[0] if args.instrument == 'HOST' else args.instrument
instrument = None if args.instrument=='main' else args.instrument
server.run(int(args.port), SEHistory(), InfluxGraph, Client, single_instrument=instrument, secop=SecopInteractor)

View File

@ -54,9 +54,12 @@ class SecopInteractor(SecopClient):
self.module_updates = set()
self.param_updates = set()
self.updates = {}
self.connect()
node_map.update({k: self for k in self.modules})
self.register_callback(None, updateItem=self.updateItem)
try:
self.connect()
node_map.update({k: self for k in self.modules})
self.register_callback(None, updateItem=self.updateItem)
except Exception as e:
print(repr(e))
def add_main_components(self, components):
for name, desc in self.modules.items():
@ -150,17 +153,17 @@ class SecopInteractor(SecopClient):
if module not in self.modules:
return None
logging.info('SENDCOMMAND %r', command)
try:
if is_param:
if is_param:
try:
entry = self.setParameterFromString(module, parameter, strvalue)
item = {'name': f'{module}:{parameter}', 'value': str(entry), 'formatted': entry.formatted()}
self.updates[module, parameter] = item
result = True
else:
result = self.execCommandFromString(module, parameter, strvalue)[0]
except Exception as e:
print(f"{e!r} converting {strvalue} to {self.modules[module]['parameters'][parameter]['datatype']}")
return result
except Exception as e:
print(f"{e!r} converting {strvalue} to {self.modules[module]['parameters'][parameter]['datatype']}")
self.updates[module, parameter] = item
return True
# called a command
formatted = self.execCommandFromString(module, parameter, strvalue)[0] # ignore qualifiers
return {'name': f'{module}:{parameter}', 'value': str(result), 'formatted': formatted}
def get_updates(self):
updates, self.updates = self.updates, {}

View File

@ -14,6 +14,11 @@ import flask
import circularlog
instruments = {ins: 8642 for ins in
['amor', 'boa', 'camea', 'dmc', 'eiger', 'focus', 'hrpt', 'sans', 'tasp', 'zebra']
}
def guess_mimetype(filename):
if filename.endswith('.js'):
mimetype = 'text/javascript'
@ -86,9 +91,13 @@ class Server:
tags['device'] = devices[0] if len(devices) == 1 else devices
return streams, tags, ','.join(device_names)
def register_client(self, instrument=None, stream=None, device=None):
def register_client(self, instrument=None, stream=None, device=None, history_only=None):
streams, tags, device_name = self.lookup_streams(instrument, stream, device)
client = self.client_cls(self, streams, instrument or '', device_name)
if (history_only or '0') != '0':
# create dummy client
client = self.client_cls(self, [], '', '')
else:
client = self.client_cls(self, streams, instrument or '', device_name)
history = self.history_cls(self, instrument, device_name, tags)
# history.db.debug = True
# all relevant methods of the history instance are saved in client.handlers
@ -106,7 +115,7 @@ class Server:
app.debug = True
logging.basicConfig(filename='webserver.log', filemode='w', level=logging.INFO,
logging.basicConfig(filename=f'logfile{port}.log', filemode='w', level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
# srv = gevent.wsgi.WSGIServer(('', port), app, keyfile='key.key', certfile='key.crt')
@ -137,7 +146,7 @@ pollinterval = 0.2
@app.route('/update')
def get_update(_=None):
# Client Adress: socket.getfqdn(flask.request.remote_addr)
kwargs = {k: flask.request.values.get(k) for k in ('instrument', 'stream', 'device')}
kwargs = {k: flask.request.values.get(k) for k in ('instrument', 'stream', 'device', 'history_only')}
client = server.register_client(**kwargs)
client.remote_info = circularlog.strtm() + " " + socket.getfqdn(flask.request.remote_addr.split(':')[-1])
@ -147,7 +156,8 @@ def get_update(_=None):
logging.info('UPDATE %s %s', client.id, socket.getfqdn(flask.request.remote_addr.split(':')[-1]))
# msg = dict(type='id', id=client.id, title=instrument.title);
# yield to_json_sse(msg)
msg = dict(type='id', id=client.id, instrument=kwargs.get('instrument') or 'n_a',
msg = dict(type='id', id=client.id,
instrument=kwargs.get('instrument') or server.single_instrument or 'n_a',
device=client.device_name)
yield to_json_sse(msg)
try:
@ -303,28 +313,31 @@ def default():
return general_file('SEAWebClient.html')
@app.route('/select_instrument')
def select_instrument():
out = ['''<html><body><table>
<style>
th {
text-align: left;
}
</style>
<tr><th>instrument</th><th colspan=99>devices</th></tr>''']
result = {}
for stream, tags in server.db.get_streams().items():
ins = tags.get('instrument', '0')
result.setdefault(ins, []).append((stream, tags.get('device')))
bare_streams = result.pop('0', [])
for ins, streams in result.items():
out.append(f'<tr><td><a href="/?ins={ins}">{ins}</a></td>')
out.extend(f'<td>{d or s}</td>' for s, d in streams)
out.append('</tr>')
for stream, device in bare_streams:
out.append(f'<tr><td><a href="/?srv={stream}">{stream}</a></td><td>{device}</td><tr>')
out.extend(['</table></body></html>', ''])
return '\n'.join(out)
#@app.route('/select_instrument')
#def select_instrument():
# out = ['''<html><body><table>
#<style>
#th {
# text-align: left;
#}
#</style>
#<tr><th>instrument</th><th colspan=99>devices</th></tr>''']
# result = {}
# for stream, tags in server.db.get_streams().items():
# ins = tags.get('instrument', '0')
# result.setdefault(ins, []).append((stream, tags.get('device')))
# bare_streams = result.pop('0', [])
# for ins, streams in result.items():
# out.append(f'<tr><td><a href="/?ins={ins}">{ins}</a></td>')
# out.extend(f'<td>{d or s}</td>' for s, d in streams)
# out.append('</tr>')
# for stream, device in bare_streams:
# out.append(f'<tr><td><a href="/?srv={stream}">{stream}</a></td><td>{device}</td><tr>')
# out.append('</table>')
# out.append('<h3>servers on the instruments:</h3>')
# out.extend([f"<a href='http://{i.lower()}.psi.ch:8642/'>{i}</a>&nbsp;\n" for i in instlist])
# out.extend(['</body></html>', ''])
# return '\n'.join(out)
@app.route('/select_experiment')
@ -335,7 +348,6 @@ content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale
<style>
th {
text-align: left;
background-color: #cccccc;
}
a {
text-decoration: none;
@ -343,11 +355,27 @@ a {
</style></head>
<body><table>
''']
showtitle = 0
ONEMONTH = 30 * 24 * 3600
def title(text):
out.append(f'<tr><td colspan=2><b>{text}</b></td></tr>')
out.append('<br><i>direct link to instruments:</i><br>')
out.extend([f'<a href="http://{ins}.psi.ch:{port}/">{ins.upper()}</a>&nbsp;\n'
for ins, port in instruments.items()])
if server.db.has_local:
out.append('<h3><a href="http://linse-c.psi.ch:8888/">linse-c (central)</a></h3>')
class prev: # just a namesapce
title = None
legend = None
def change_title(text):
if text == prev.title:
return False
if prev.legend:
out.append(f'<tr>{prev.legend}</tr>')
prev.legend = None
prev.title = text
out.append(f'<tr><td colspan=3><br><i>{text}:</i></td></tr>')
return True
# TODO: sort this by (instrument / device) and list dates
# period format: Ymd..Ymd, Ymd (single date), Ymd..now, HM..now
@ -369,27 +397,50 @@ a {
chunk_list.sort(reverse=True)
for end, beg, key, devices in chunk_list:
today, begdate, enddate = (time.strftime("%Y-%m-%d", time.localtime(t)) for t in (now, beg, end))
port = None
if key[0] == 'instrument':
ins = key[1]
port = instruments.get(ins)
left = ins.upper()
else:
left = key[1] # shown in left column
args = ['='.join(key)]
remote = None if port is None else f'http://{ins}.psi.ch:{port}'
history_only = bool(remote)
if end > now:
if begdate == today:
daterange = f'since {time.strftime("%H:%M", time.localtime(beg))}'
else:
daterange = f'since {begdate}'
if showtitle == 0:
title('currently running')
showtitle = 1
change_title('currently running')
else:
args.append(f'time={beg},{end}')
history_only = True
remote = None
daterange = begdate if begdate == enddate else f'{begdate}...{enddate}'
if end > now - ONEMONTH:
if showtitle == 1:
title('older than 30 days')
showtitle = 2
change_title('recently running (history graphics only)')
else:
change_title('older than 30 days')
if history_only:
args.append('hr=1')
out.append(f'<tr><th><a href="/?{"&".join(args)}">{key[1]} / {" ".join(devices)}</a></th>')
def link(label):
return f'<a href="/?{"&".join(args)}">{label}</a>'
label = " ".join(devices)
if remote:
prev.legend = '<td></td><td></td><td colspan=2>linse-c*: <i>history graphics only</i></td>'
out.append(f'<tr><td><a href="{remote}">{ins.upper()}</a></td>'
f'<td>{label}</td><td>{link("linse-c*")}</td>')
else:
out.append(f'<tr><td>{link(left)}</td><td colspan=2>{label}</td>')
out.append(f'<td>{daterange}</td></tr>')
if timerange:
out.append(f'<h3><a href="/select_experiment?time=all">earlier dates</a></h3><br>')
out.extend(['</table></body></html>', ''])
out.append('</table>')
out.extend(['</body></html>', ''])
except Exception as e:
logging.error('%s', traceback.format_exc())
circularlog.log()