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
This commit is contained in:
69
base.py
69
base.py
@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import logging
|
import uuid
|
||||||
|
|
||||||
ONEYEAR = 366 * 24 * 3600
|
ONEYEAR = 366 * 24 * 3600
|
||||||
|
|
||||||
@ -35,16 +35,63 @@ class Logger(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Instrument:
|
class HandlerBase:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.clients = {}
|
self.handlers = {k[2:]: getattr(self, k) for k in dir(type(self)) if k.startswith('w_')}
|
||||||
|
|
||||||
def remove(self, client):
|
|
||||||
try:
|
|
||||||
del self.clients[client.id]
|
|
||||||
except KeyError:
|
|
||||||
logging.warning('client already removed %s', client.id)
|
|
||||||
|
|
||||||
def register(self, client):
|
class Client(HandlerBase):
|
||||||
self.clients[client.id] = client
|
def __init__(self, server, streams, instrument_name, device_name):
|
||||||
return client
|
super().__init__()
|
||||||
|
self.id = uuid.uuid4().hex[0:15]
|
||||||
|
self.nodes = {}
|
||||||
|
self.node_map = {}
|
||||||
|
if streams:
|
||||||
|
for uri in streams:
|
||||||
|
urisplit = uri.rsplit('://')
|
||||||
|
kind = urisplit[0] if len(urisplit) == 2 else 'secop'
|
||||||
|
node = server.interactor_classes[kind](uri, self.node_map)
|
||||||
|
self.nodes[uri] = node
|
||||||
|
self.server = server
|
||||||
|
self.instrument_name = instrument_name
|
||||||
|
self.device_name = device_name # do not know if this is needed
|
||||||
|
self.updates = {}
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
updates = sum((n.get_updates() for n in self.nodes.values()), start=[])
|
||||||
|
result = [dict(type='update', updates=updates)] if updates else []
|
||||||
|
graph_updates = self.handlers.get('graphpoll', object)()
|
||||||
|
if graph_updates:
|
||||||
|
result.append(graph_updates)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def w_getblock(self, path):
|
||||||
|
path = path.split(',')[-1] # TODO: why this?
|
||||||
|
if path == "main": # TODO: change to "-main-"?
|
||||||
|
components = []
|
||||||
|
for node in self.nodes.values():
|
||||||
|
node.add_main_components(components)
|
||||||
|
return dict(type='draw', path='main', title='modules', components=components)
|
||||||
|
node = self.node_map[path]
|
||||||
|
return dict(type='draw', path=path, title=path, components=node.get_components(path))
|
||||||
|
|
||||||
|
def w_updateblock(self, path):
|
||||||
|
if path == 'main': # TODO: change to "-main-"?
|
||||||
|
for node in self.nodes.values():
|
||||||
|
node.update_main()
|
||||||
|
else:
|
||||||
|
node = self.node_map[path]
|
||||||
|
node.update_params(path)
|
||||||
|
return dict(type='accept-block')
|
||||||
|
|
||||||
|
def w_console(self): # TODO: check if still used
|
||||||
|
return dict(type='accept-console')
|
||||||
|
|
||||||
|
def w_sendcommand(self, command):
|
||||||
|
for node in self.nodes.values():
|
||||||
|
if node.handle_command(command):
|
||||||
|
break
|
||||||
|
return dict(type='accept-command')
|
||||||
|
|
||||||
|
def info(self):
|
||||||
|
return ["na"]
|
||||||
|
@ -17,9 +17,9 @@ function buildUpdateConnection() {
|
|||||||
|
|
||||||
// Establishes server-sent-event-connection, which is used for all sorts of
|
// Establishes server-sent-event-connection, which is used for all sorts of
|
||||||
// updates and exists as long as the client is running.
|
// updates and exists as long as the client is running.
|
||||||
// Executed at programstart (see also SEAWebClientMain.js).
|
// Executed at program start (see also SEAWebClientMain.js).
|
||||||
|
|
||||||
var path = "http://" + hostPort + "/update";
|
var path = "http://" + hostPort + "/update?" + window.clientTags;
|
||||||
if (debugCommunication) {
|
if (debugCommunication) {
|
||||||
console.log("%cto server (SSE): " + path,
|
console.log("%cto server (SSE): " + path,
|
||||||
"color:white;background:lightblue");
|
"color:white;background:lightblue");
|
||||||
@ -29,8 +29,7 @@ function buildUpdateConnection() {
|
|||||||
var src = new EventSource(path);
|
var src = new EventSource(path);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
alertify.prompt(
|
alertify.prompt("NETWORK ERROR",
|
||||||
"NETWORK ERROR",
|
|
||||||
"Failed to establish connection to data-server at the given address!"
|
"Failed to establish connection to data-server at the given address!"
|
||||||
+ "Try to enter HOST and PORT of the data-server manually!",
|
+ "Try to enter HOST and PORT of the data-server manually!",
|
||||||
hostPort, function(evt, value) {
|
hostPort, function(evt, value) {
|
||||||
@ -50,9 +49,7 @@ function buildUpdateConnection() {
|
|||||||
src.onerror = function(e) {
|
src.onerror = function(e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
console.log('EVTSRC error')
|
console.log('EVTSRC error')
|
||||||
alertify
|
alertify.prompt("NETWORK ERROR",
|
||||||
.prompt(
|
|
||||||
"NETWORK ERROR",
|
|
||||||
"Failed to establish connection to data-server at the given address!"
|
"Failed to establish connection to data-server at the given address!"
|
||||||
+ "Try to enter HOST and PORT of the data-server manually!",
|
+ "Try to enter HOST and PORT of the data-server manually!",
|
||||||
hostPort, function(evt, value) {
|
hostPort, function(evt, value) {
|
||||||
@ -406,10 +403,9 @@ function successHandler(s, message) {
|
|||||||
begin = timeRange[0] - timeRange[1];
|
begin = timeRange[0] - timeRange[1];
|
||||||
select.value = begin;
|
select.value = begin;
|
||||||
// Server-request for variable-list.*/
|
// Server-request for variable-list.*/
|
||||||
console.log('TIME', window['clientTags'], timeRange)
|
console.log('TIME', timeRange)
|
||||||
reqJSONPOST(0, "http://" + hostPort + "/getvars",
|
reqJSONPOST(0, "http://" + hostPort + "/getvars",
|
||||||
"time=" + timeRange[0] + ',' + timeRange[1]
|
"time=" + timeRange[0] + ',' + timeRange[1]
|
||||||
+ window['clientTags']
|
|
||||||
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
||||||
+ "&id=" + clientID, successHandler, errorHandler);
|
+ "&id=" + clientID, successHandler, errorHandler);
|
||||||
break;
|
break;
|
||||||
|
@ -490,7 +490,7 @@ let graphs = (function (){
|
|||||||
if(idx != -1){ //if the clicked block is displayed somewhere, we create a selection window
|
if(idx != -1){ //if the clicked block is displayed somewhere, we create a selection window
|
||||||
createSelection(idx); // We will create a selection at the gindex
|
createSelection(idx); // We will create a selection at the gindex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createGraph(gindex, block); // we create at the current shown selector (gindex), the graph corresponding to the one clicked (block)
|
createGraph(gindex, block); // we create at the current shown selector (gindex), the graph corresponding to the one clicked (block)
|
||||||
})
|
})
|
||||||
selection.appendChild(bel);
|
selection.appendChild(bel);
|
||||||
@ -547,7 +547,7 @@ let graphs = (function (){
|
|||||||
+ "&variables=" + varlist
|
+ "&variables=" + varlist
|
||||||
+ "&interval=" + resolution
|
+ "&interval=" + resolution
|
||||||
+ "&id=" + clientID).getJSON().then(function(data){
|
+ "&id=" + clientID).getJSON().then(function(data){
|
||||||
//console.log('Graph', block, data)
|
// console.log('Graph', block, data);
|
||||||
let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
|
let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
|
||||||
graph_array[gindex] = graph;
|
graph_array[gindex] = graph;
|
||||||
|
|
||||||
@ -559,7 +559,6 @@ let graphs = (function (){
|
|||||||
for(let e of data.graph[key]){
|
for(let e of data.graph[key]){
|
||||||
pdata.push({x: e[0]*1000, y: e[1]});
|
pdata.push({x: e[0]*1000, y: e[1]});
|
||||||
}
|
}
|
||||||
|
|
||||||
addDataset(gindex, key, pdata, dict[key])
|
addDataset(gindex, key, pdata, dict[key])
|
||||||
// if(pdata.length > 0){
|
// if(pdata.length > 0){
|
||||||
// addDataset(gindex, key, pdata, dict[key])
|
// addDataset(gindex, key, pdata, dict[key])
|
||||||
@ -919,7 +918,6 @@ let graphs = (function (){
|
|||||||
|
|
||||||
AJAX("http://" + hostPort + "/getvars").postForm(
|
AJAX("http://" + hostPort + "/getvars").postForm(
|
||||||
"time=" + msRightTimestampGetVars/1000
|
"time=" + msRightTimestampGetVars/1000
|
||||||
+ window['clientTags']
|
|
||||||
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
||||||
+ "&id="+ clientID).then(function(data){
|
+ "&id="+ clientID).then(function(data){
|
||||||
blocks = data.blocks;
|
blocks = data.blocks;
|
||||||
@ -998,7 +996,6 @@ let graphs = (function (){
|
|||||||
AJAX("http://" + hostPort + "/getvars").postForm(
|
AJAX("http://" + hostPort + "/getvars").postForm(
|
||||||
"time=" + msRightTimestamp/1000 + "&userconfiguration="
|
"time=" + msRightTimestamp/1000 + "&userconfiguration="
|
||||||
+ JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
+ JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
||||||
+ window['clientTags']
|
|
||||||
+ "&id="+ clientID).then(function(data){
|
+ "&id="+ clientID).then(function(data){
|
||||||
currentMaxTime = msRightTimestamp + 60000;
|
currentMaxTime = msRightTimestamp + 60000;
|
||||||
currentMinTime = msLeftTimestamp;
|
currentMinTime = msLeftTimestamp;
|
||||||
@ -1304,7 +1301,6 @@ let graphs = (function (){
|
|||||||
AJAX("http://" + hostPort + "/getvars").postForm(
|
AJAX("http://" + hostPort + "/getvars").postForm(
|
||||||
"time=" + currentMaxTime/1000
|
"time=" + currentMaxTime/1000
|
||||||
+ "&userconfiguration=" + JSON.stringify(userConfiguration)
|
+ "&userconfiguration=" + JSON.stringify(userConfiguration)
|
||||||
+ window['clientTags']
|
|
||||||
+ "&id="+ clientID).then(function(data){
|
+ "&id="+ clientID).then(function(data){
|
||||||
blocks = data.blocks;
|
blocks = data.blocks;
|
||||||
document.getElementById("device").innerHTML = data.device
|
document.getElementById("device").innerHTML = data.device
|
||||||
|
@ -91,18 +91,21 @@ new Settings()
|
|||||||
.treat("hideRightPart", "hr", to_bool, false) //used to completely disable the right part
|
.treat("hideRightPart", "hr", to_bool, false) //used to completely disable the right part
|
||||||
.treat("wideGraphs", "wg", to_bool, false) //used to toggle the size of the graphs part
|
.treat("wideGraphs", "wg", to_bool, false) //used to toggle the size of the graphs part
|
||||||
.treat("showAsync", "sa", to_bool, false)
|
.treat("showAsync", "sa", to_bool, false)
|
||||||
.treat("device", "dev", 0, "*")
|
.treat("device", "dev", 0, "")
|
||||||
.treat("server", "srv", 0, "*")
|
.treat("server", "srv", 0, "")
|
||||||
.treat("instrument", "instrument", 0, "")
|
.treat("instrument", "instrument", 0, "")
|
||||||
.treat("timerange", "time", 0, "-1800,0")
|
.treat("timerange", "time", 0, "-1800,0")
|
||||||
|
|
||||||
if (window['instrument']) {
|
if (window.instrument) {
|
||||||
window['clientTags'] = "&instrument=" + window["instrument"];
|
window.clientTags = "&instrument=" + window.instrument;
|
||||||
} else {
|
} else {
|
||||||
window['clientTags'] = "&stream=" + window["server"] + "&device=" + window["device"];
|
let args = '';
|
||||||
|
if (window.server) { args += "&stream=" + window.server; }
|
||||||
|
if (window.device) { args += "&device=" + window.device; }
|
||||||
|
window.clientTags = args;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('TAGS', window['clientTags']);
|
console.log('TAGS', window.clientTags);
|
||||||
|
|
||||||
function loadFirstBlocks() {
|
function loadFirstBlocks() {
|
||||||
if (debug_main_daniel) {
|
if (debug_main_daniel) {
|
||||||
@ -112,11 +115,19 @@ function loadFirstBlocks() {
|
|||||||
if (showMain) pushInitCommand("getblock?path=main&", "main")
|
if (showMain) pushInitCommand("getblock?path=main&", "main")
|
||||||
if (showConsole) pushInitCommand("console?", "console")
|
if (showConsole) pushInitCommand("console?", "console")
|
||||||
if (nColumns == 1) { // probably mobile phone}
|
if (nColumns == 1) { // probably mobile phone}
|
||||||
if (showGraphics) pushInitCommand("gettime?time=" + window["timerange"] + "&", "graphics")
|
if (showGraphics) pushInitCommand("gettime?time=" + window.timerange + "&", "graphics")
|
||||||
if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
|
if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
|
||||||
|
var goFS = document.getElementById('header');
|
||||||
|
goFS.addEventListener(
|
||||||
|
'click',
|
||||||
|
function () {
|
||||||
|
document.body.requestFullscreen();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
|
if (showOverview) pushInitCommand("getblock?path=_overview&", "overview")
|
||||||
if (showGraphics) pushInitCommand("gettime?time=" + window["timerange"] + "&", "graphics")
|
if (showGraphics) pushInitCommand("gettime?time=" + window.timerange + "&", "graphics")
|
||||||
// last is shown first
|
// last is shown first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,7 +180,7 @@ window.onload = function() {
|
|||||||
|
|
||||||
let crossElement = document.getElementById("close-cross");
|
let crossElement = document.getElementById("close-cross");
|
||||||
|
|
||||||
if(window["hideRightPart"]){
|
if(window.hideRightPart){
|
||||||
document.body.removeChild(crossElement);
|
document.body.removeChild(crossElement);
|
||||||
}else{
|
}else{
|
||||||
crossElement.onclick = function(){
|
crossElement.onclick = function(){
|
||||||
@ -193,7 +204,7 @@ window.onload = function() {
|
|||||||
elements[2].style.display = "none"; // hide parameters
|
elements[2].style.display = "none"; // hide parameters
|
||||||
}
|
}
|
||||||
}else{ // else it toggles the graphs window's size and triggers the adjustGrid()
|
}else{ // else it toggles the graphs window's size and triggers the adjustGrid()
|
||||||
window["wideGraphs"] = !window['wideGraphs'];
|
window.wideGraphs = ! window.wideGraphs;
|
||||||
adjustGrid();
|
adjustGrid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,9 +218,8 @@ window.onload = function() {
|
|||||||
|
|
||||||
// var homeButton = document.getElementById("home-icon");
|
// var homeButton = document.getElementById("home-icon");
|
||||||
|
|
||||||
// TODO : uncomment this code with the right URL to navigate to when the way to select the instrument will be decided.
|
|
||||||
homeButton.onclick = function () {
|
homeButton.onclick = function () {
|
||||||
window.location = "/select";
|
window.location = "/select_experiment";
|
||||||
};
|
};
|
||||||
buildUpdateConnection();
|
buildUpdateConnection();
|
||||||
// if (location.hash) {
|
// if (location.hash) {
|
||||||
|
241
influxgraph.py
241
influxgraph.py
@ -3,20 +3,19 @@ import logging
|
|||||||
import json
|
import json
|
||||||
import io
|
import io
|
||||||
import uuid
|
import uuid
|
||||||
from configparser import ConfigParser
|
# from configparser import ConfigParser
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from sehistory.seinflux import SEHistory
|
from sehistory.seinflux import fmtime
|
||||||
from colors import assign_colors_to_curves
|
from colors import assign_colors_to_curves
|
||||||
from chart_config import ChartConfig
|
from chart_config import ChartConfig
|
||||||
from base import Instrument, get_abs_time
|
from base import get_abs_time, HandlerBase
|
||||||
from secop import SecopClient, SecopInstrument
|
|
||||||
|
|
||||||
|
|
||||||
def split_tags(tags):
|
def split_tags(tags):
|
||||||
return {k: v.split(',') for k, v in tags.items()}
|
return {k: v.split(',') for k, v in tags.items()}
|
||||||
|
|
||||||
|
|
||||||
class InfluxGraph:
|
class InfluxGraph(HandlerBase):
|
||||||
"""Class implementing the logic of the different routes that are called by the client to retrieve graph data with InfluxDB.
|
"""Class implementing the logic of the different routes that are called by the client to retrieve graph data with InfluxDB.
|
||||||
|
|
||||||
Global constants :
|
Global constants :
|
||||||
@ -43,22 +42,37 @@ class InfluxGraph:
|
|||||||
ACTUAL = 1
|
ACTUAL = 1
|
||||||
LIVE = 2
|
LIVE = 2
|
||||||
|
|
||||||
def __init__(self, instrument):
|
def __init__(self, server, instrument, device_name, tags):
|
||||||
self.instrument = instrument
|
"""create instance for retrieving history
|
||||||
self.db = instrument.db
|
|
||||||
instrument_name = instrument.title
|
:param db: a database client (SEInflux instance)
|
||||||
|
:param instrument: the name of anm instrument or None
|
||||||
|
:param streams: a stream or comma separated list of streams
|
||||||
|
:param devices: a device name ar a comma separated list of devices
|
||||||
|
:param device_name: (comma separated) device name for labelling
|
||||||
|
typically only one of the 3 last parameters are needed
|
||||||
|
if more are specified, all of them must be fulfilled
|
||||||
|
"""
|
||||||
|
super().__init__() # put methods w_... to handlers
|
||||||
|
self.handlers['graphpoll'] = self.graphpoll
|
||||||
|
self.server = server
|
||||||
|
self.db = server.db
|
||||||
# self.influx_data_getter = influx_data_getter
|
# self.influx_data_getter = influx_data_getter
|
||||||
self.chart_configs = [ChartConfig("./config/generic.ini")]
|
self.chart_configs = [ChartConfig("./config/generic.ini")]
|
||||||
try:
|
self.instrument = instrument
|
||||||
self.chart_configs.append(ChartConfig(f"./config/{instrument_name}.ini"))
|
self.device_name = device_name
|
||||||
except KeyError:
|
if instrument: # TODO: should it not be better to have inifiles per device?
|
||||||
pass
|
try:
|
||||||
|
self.chart_configs.append(ChartConfig(f"./config/{instrument}.ini"))
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
self.livemode = self.HISTORICAL
|
self.livemode = self.HISTORICAL
|
||||||
self.last_values = {} # dict <variable> of last known point (<time>, <value>)
|
self.last_values = {} # dict <variable> of last known point (<time>, <value>)
|
||||||
self.last_time = {} # dict <stream> of last received time
|
self.last_time = {} # dict <stream> of last received time
|
||||||
self.last_minute = 0
|
self.last_minute = 0
|
||||||
self.last_update = 0 # time of last call with a result
|
self.last_update = 0 # time of last call with a result
|
||||||
self.tags = {} # tags for query (determines device and/or server)
|
self.tags = None
|
||||||
|
self.init_tags = tags
|
||||||
|
|
||||||
def w_graph(self, variables, time="-1800,0", interval=None):
|
def w_graph(self, variables, time="-1800,0", interval=None):
|
||||||
"""Get the curves given by variables in the time range "time"
|
"""Get the curves given by variables in the time range "time"
|
||||||
@ -84,14 +98,12 @@ class InfluxGraph:
|
|||||||
logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode)
|
logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode)
|
||||||
if interval:
|
if interval:
|
||||||
interval = float(interval)
|
interval = float(interval)
|
||||||
print('CURVES', start - now, end - now, self.tags)
|
|
||||||
result = self.db.curves(start, end, queried_variables, merge='_measurement',
|
result = self.db.curves(start, end, queried_variables, merge='_measurement',
|
||||||
interval=interval or None, **self.tags)
|
interval=interval or None, **self.tags)
|
||||||
print('LEN', len(result))
|
|
||||||
self.update_last(result)
|
self.update_last(result)
|
||||||
self.db.complete(result, self.last_time, 'stream')
|
self.db.complete(result, self.last_time, 'stream')
|
||||||
self.last_minute = now // 60
|
self.last_minute = now // 60
|
||||||
return dict(type='graph-draw', graph={k: v for k, v in result.items()})
|
return dict(type='graph-draw', graph={k: result[k] for k in queried_variables if k in result})
|
||||||
|
|
||||||
def update_last(self, curve_dict):
|
def update_last(self, curve_dict):
|
||||||
"""update last values per variable and last time per stream"""
|
"""update last values per variable and last time per stream"""
|
||||||
@ -118,7 +130,7 @@ class InfluxGraph:
|
|||||||
return dict(type='time', time=get_abs_time(
|
return dict(type='time', time=get_abs_time(
|
||||||
[float(t) for t in time.split(',')]))
|
[float(t) for t in time.split(',')]))
|
||||||
|
|
||||||
def w_getvars(self, time, userconfiguration=None, instrument=None, **tags):
|
def w_getvars(self, time, userconfiguration=None, **_):
|
||||||
"""Get the curve names available at a given point in time
|
"""Get the curve names available at a given point in time
|
||||||
|
|
||||||
with a possible user configuration on the client side.
|
with a possible user configuration on the client side.
|
||||||
@ -145,22 +157,17 @@ class InfluxGraph:
|
|||||||
if userconfiguration is not None:
|
if userconfiguration is not None:
|
||||||
userconfiguration = json.loads(userconfiguration)
|
userconfiguration = json.loads(userconfiguration)
|
||||||
|
|
||||||
self.tags = split_tags(tags)
|
if self.instrument:
|
||||||
if instrument:
|
streams, tags, self.device_name = self.server.lookup_streams(self.instrument, **self.init_tags)
|
||||||
self.tags['stream'] = list(self.db.get_streams(instrument))
|
self.tags = {**self.init_tags, **tags}
|
||||||
print('TAGS', self.tags)
|
|
||||||
blocks = self.get_available_variables(start_time, end_time, self.chart_configs, userconfiguration)
|
blocks = self.get_available_variables(start_time, end_time, self.chart_configs, userconfiguration)
|
||||||
device_name = self.tags.get('device', '<unknown>')
|
|
||||||
# initialize self.last_values to keep track of the available variables
|
# initialize self.last_values to keep track of the available variables
|
||||||
self.last_values = {var["name"]: [0, None] for block in blocks for var in block["curves"]}
|
self.last_values = {var["name"]: [0, None] for block in blocks for var in block["curves"]}
|
||||||
assign_colors_to_curves(blocks)
|
assign_colors_to_curves(blocks)
|
||||||
result = dict(type='var_list')
|
|
||||||
result['blocks'] = blocks
|
|
||||||
result['device'] = device_name
|
|
||||||
# print('DEVICE', device_name, tags)
|
# print('DEVICE', device_name, tags)
|
||||||
# for block in blocks:
|
# for block in blocks:
|
||||||
# print(block['tag'], [c['name'] for c in block['curves']])
|
# print(block['tag'], [c['name'] for c in block['curves']])
|
||||||
return result
|
return {'type': 'var_list', 'blocks': blocks, 'device': self.device_name}
|
||||||
|
|
||||||
def get_available_variables(self, start_time, end_time, chart_configs=None, user_config=None):
|
def get_available_variables(self, start_time, end_time, chart_configs=None, user_config=None):
|
||||||
"""Gets the available variables
|
"""Gets the available variables
|
||||||
@ -182,15 +189,16 @@ class InfluxGraph:
|
|||||||
|
|
||||||
Returns :
|
Returns :
|
||||||
[{"tag":(str), "unit":(str), "curves":[{"name":(str), "label":(str), "color":(str)}]}] :
|
[{"tag":(str), "unit":(str), "curves":[{"name":(str), "label":(str), "color":(str)}]}] :
|
||||||
a list of dictionnaries, each one representing
|
a list of dicts, each one representing
|
||||||
a block of curves with their name, their label and their color to display,
|
a block of curves with their name, their label and their color to display,
|
||||||
grouped by their category if given or unit (in tag).
|
grouped by their category if given or unit (in tag).
|
||||||
"""
|
"""
|
||||||
if start_time == end_time:
|
if start_time == end_time:
|
||||||
start_time = end_time - 3600
|
start_time = end_time - 3600
|
||||||
result = self.db.curves(start_time, end_time, _measurement=None, _field='float', **self.tags)
|
result = self.db.curves(start_time, end_time, _measurement=None,
|
||||||
|
merge='_measurement', **self.tags)
|
||||||
assert all(c.key_names[0] == '_measurement' for c in result.values())
|
assert all(c.key_names[0] == '_measurement' for c in result.values())
|
||||||
variables = {k[0] for k in result}
|
variables = {k: t.tags.get('unit') for k, t in result.items()}
|
||||||
config = {}
|
config = {}
|
||||||
if chart_configs:
|
if chart_configs:
|
||||||
for chart_config in chart_configs:
|
for chart_config in chart_configs:
|
||||||
@ -215,38 +223,59 @@ class InfluxGraph:
|
|||||||
cat = '*'
|
cat = '*'
|
||||||
elif not cat:
|
elif not cat:
|
||||||
return
|
return
|
||||||
|
unit = unit or '1'
|
||||||
tag = cat.replace('*', unit)
|
tag = cat.replace('*', unit)
|
||||||
grp = groups.get(tag)
|
grp = groups.get(tag)
|
||||||
if grp is None:
|
if grp is None:
|
||||||
curves = []
|
crv_dict = {}
|
||||||
groups[tag] = {'tag': cat.replace('*', unit), 'unit': unit, 'curves': curves}
|
groups[tag] = {'tag': cat.replace('*', unit), 'unit': unit, 'curves': crv_dict}
|
||||||
else:
|
else:
|
||||||
curves = grp['curves']
|
crv_dict = grp['curves']
|
||||||
curves.append({'name': name, 'unit': unit, 'label': label or name})
|
crv_dict[name] = {'name': name, 'unit': unit, 'label': label or name}
|
||||||
|
|
||||||
# treat variables in config first (in their order!)
|
# treat variables in config first (in their order!)
|
||||||
result = {}
|
|
||||||
for key, cfg in config.items():
|
for key, cfg in config.items():
|
||||||
cat = cfg.pop('cat', None)
|
cat = cfg.pop('cat', None)
|
||||||
unit = cfg.get('unit', '1')
|
cfgunit = cfg.pop('unit', '')
|
||||||
if '.' in key:
|
if '.' in key:
|
||||||
if key in variables:
|
unit = variables.pop(key, object)
|
||||||
add_to_groups(key, cat, **cfg)
|
if unit is not object:
|
||||||
variables.discard(key)
|
add_to_groups(key, cat, cfgunit or unit, **cfg)
|
||||||
else:
|
else:
|
||||||
var = f'{key}.value'
|
var = f'{key}.value'
|
||||||
if var in variables:
|
unit = variables.pop(var, object)
|
||||||
|
if unit is not object:
|
||||||
label = cfg.pop('label', None) or key
|
label = cfg.pop('label', None) or key
|
||||||
add_to_groups(var, cat, label=label, **cfg)
|
add_to_groups(var, cat, cfgunit or unit, label=label, **cfg)
|
||||||
variables.discard(var)
|
|
||||||
var = f'{key}.target'
|
var = f'{key}.target'
|
||||||
if var in variables:
|
unit = variables.pop(var, object)
|
||||||
|
if unit is not object:
|
||||||
cfg.pop('color', None)
|
cfg.pop('color', None)
|
||||||
add_to_groups(var, cat, **cfg)
|
add_to_groups(var, cat, cfgunit or unit, **cfg)
|
||||||
variables.discard(var)
|
for var, unit in variables.items():
|
||||||
for var in variables:
|
add_to_groups(var, unit=unit)
|
||||||
add_to_groups(var)
|
# make order a bit more common
|
||||||
return list(groups.values())
|
result = []
|
||||||
|
for key in ['K', 'T', 'W', 'ln/min'] + list(groups):
|
||||||
|
if key in groups:
|
||||||
|
group = groups.pop(key)
|
||||||
|
curve_dict = group['curves']
|
||||||
|
curves = []
|
||||||
|
# get first '.value' parameters and add targets if available
|
||||||
|
ordered_keys = [f'{m}.value' for m in ('tt', 'T', 'ts', 'Ts')]
|
||||||
|
for name in ordered_keys + list(curve_dict):
|
||||||
|
if name.endswith('.value'):
|
||||||
|
try:
|
||||||
|
curves.append(curve_dict.pop(name))
|
||||||
|
curves.append(curve_dict.pop(f'{name[:-6]}.target'))
|
||||||
|
except KeyError:
|
||||||
|
pass # skip not existing or already removed items
|
||||||
|
# add remaining curves
|
||||||
|
curves.extend(curve_dict.values())
|
||||||
|
print(key, curves)
|
||||||
|
group['curves'] = curves
|
||||||
|
result.append(group)
|
||||||
|
return result
|
||||||
|
|
||||||
def w_updategraph(self):
|
def w_updategraph(self):
|
||||||
"""Set the current visualisation mode to LIVE if not in HISTORICAL mode.
|
"""Set the current visualisation mode to LIVE if not in HISTORICAL mode.
|
||||||
@ -348,17 +377,18 @@ class InfluxGraph:
|
|||||||
return dict(type='graph-update', time=last_time, graph=result)
|
return dict(type='graph-update', time=last_time, graph=result)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
class InfluxInstrument(Instrument):
|
|
||||||
|
|
||||||
def __init__(self, instr_name, inst_config=None):
|
# class InfluxInstrument(HandlerBase):
|
||||||
super().__init__()
|
#
|
||||||
self.db = InfluxDB()
|
# def __init__(self, instr_name, inst_config=None):
|
||||||
# self.influx_data_getter = InfluxDataGetter(self.db, instr_name)
|
# super().__init__()
|
||||||
self.title = instr_name
|
# self.db = InfluxDB()
|
||||||
self.device = self.influx_data_getter.get_device_name(int(current_time()))
|
# # self.influx_data_getter = InfluxDataGetter(self.db, instr_name)
|
||||||
|
# self.title = instr_name
|
||||||
def new_client(self):
|
# self.device = self.influx_data_getter.get_device_name(int(current_time()))
|
||||||
return self.register(InfluxClient(self))
|
#
|
||||||
|
# def new_client(self):
|
||||||
|
# return self.register(InfluxClient(self))
|
||||||
|
|
||||||
|
|
||||||
class InfluxParams:
|
class InfluxParams:
|
||||||
@ -383,53 +413,50 @@ class InfluxParams:
|
|||||||
return dict(type='accept-command')
|
return dict(type='accept-command')
|
||||||
|
|
||||||
|
|
||||||
class InfluxClient(InfluxParams, InfluxGraph):
|
# class InfluxClient(InfluxParams, InfluxGraph):
|
||||||
def __init__(self, instrument):
|
# def __init__(self, instrument):
|
||||||
InfluxParams.__init__(self)
|
# InfluxParams.__init__(self)
|
||||||
InfluxGraph.__init__(self, instrument)
|
# InfluxGraph.__init__(self, instrument)
|
||||||
|
#
|
||||||
def poll(self):
|
# def poll(self):
|
||||||
messages = self.queue
|
# messages = self.queue
|
||||||
self.queue = []
|
# self.queue = []
|
||||||
msg = self.graphpoll()
|
# msg = self.graphpoll()
|
||||||
if msg:
|
# if msg:
|
||||||
messages.append(msg)
|
# messages.append(msg)
|
||||||
return messages
|
# return messages
|
||||||
|
#
|
||||||
|
#
|
||||||
class SecopInfluxClient(SecopClient, InfluxGraph):
|
# class SecopInfluxClient(SecopClient, InfluxGraph):
|
||||||
def __init__(self, instrument):
|
# def __init__(self, instrument):
|
||||||
SecopClient.__init__(self, instrument)
|
# SecopClient.__init__(self, instrument)
|
||||||
InfluxGraph.__init__(self, instrument)
|
# InfluxGraph.__init__(self, instrument)
|
||||||
|
#
|
||||||
def poll(self):
|
# def poll(self):
|
||||||
messages = super().poll()
|
# messages = super().poll()
|
||||||
msg = self.graphpoll()
|
# msg = self.graphpoll()
|
||||||
if msg:
|
# if msg:
|
||||||
messages.append(msg)
|
# messages.append(msg)
|
||||||
return messages
|
# return messages
|
||||||
|
#
|
||||||
|
#
|
||||||
class SecopInfluxInstrument(SecopInstrument):
|
# class SecopInfluxInstrument(SecopInstrument):
|
||||||
|
#
|
||||||
def __init__(self, inst_name, instrument_config):
|
# def __init__(self, inst_name, instrument_config):
|
||||||
super().__init__(inst_name, instrument_config)
|
# super().__init__(inst_name, instrument_config)
|
||||||
config = ConfigParser()
|
# config = ConfigParser()
|
||||||
config.optionxform = str
|
# config.optionxform = str
|
||||||
config.read("./config/influx.ini")
|
# config.read("./config/influx.ini")
|
||||||
section = config["INFLUX"]
|
# section = config["INFLUX"]
|
||||||
self.db = SEHistory()
|
# self.db = SEHistory()
|
||||||
# self.db = InfluxDBWrapper(uri=section["url"], token=section["token"],
|
# # self.db = InfluxDBWrapper(uri=section["url"], token=section["token"],
|
||||||
# org=section["org"], bucket=section['bucket'])
|
# # org=section["org"], bucket=section['bucket'])
|
||||||
# self.influx_data_getter = InfluxDataGetter(self.db, inst_name)
|
# # self.influx_data_getter = InfluxDataGetter(self.db, inst_name)
|
||||||
# self.device = self.influx_data_getter.get_device_name(int(current_time()))
|
# # self.device = self.influx_data_getter.get_device_name(int(current_time()))
|
||||||
|
#
|
||||||
def new_client(self):
|
# def get_streams(self, timestamp=None):
|
||||||
return self.register(SecopInfluxClient(self))
|
# return self.db.get_streams(None, timestamp)
|
||||||
|
#
|
||||||
def get_streams(self, timestamp=None):
|
# def get_experiments(self, start=None, stop=None):
|
||||||
return self.db.get_streams(None, timestamp)
|
# return self.db.get_experiments(start, stop)
|
||||||
|
|
||||||
def get_experiments(self, start=None, stop=None):
|
|
||||||
return self.db.get_experiments(start, stop)
|
|
||||||
|
|
||||||
|
@ -1,9 +1,43 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import pathlib
|
import argparse
|
||||||
sys.path.insert(0, str((pathlib.Path(__file__) / '..').resolve()))
|
import socket
|
||||||
import webserver
|
from webserver import server
|
||||||
from influxgraph import SecopInfluxInstrument
|
from base import Client
|
||||||
|
from influxgraph import InfluxGraph
|
||||||
|
from secop import SecopInteractor
|
||||||
|
from sehistory.seinflux import SEHistory
|
||||||
|
|
||||||
webserver.instrument = webserver.main(SecopInfluxInstrument)
|
|
||||||
|
def parseArgv(argv):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="start a webserver for history and interaction",
|
||||||
|
)
|
||||||
|
# loggroup = parser.add_mutually_exclusive_group()
|
||||||
|
# loggroup.add_argument("-v", "--verbose",
|
||||||
|
# help="Output lots of diagnostic information",
|
||||||
|
# action='store_true', default=False)
|
||||||
|
# loggroup.add_argument("-q", "--quiet", help="suppress non-error messages",
|
||||||
|
# action='store_true', default=False)
|
||||||
|
parser.add_argument("port",
|
||||||
|
type=str,
|
||||||
|
help="port number to serve\n")
|
||||||
|
# parser.add_argument('-d',
|
||||||
|
# '--daemonize',
|
||||||
|
# action='store_true',
|
||||||
|
# help='Run as daemon',
|
||||||
|
# default=False)
|
||||||
|
parser.add_argument('-i',
|
||||||
|
'--instrument',
|
||||||
|
action='store',
|
||||||
|
help="instrument, if running on an instrument computer\n"
|
||||||
|
"if the value is HOST, take the host name as instrument name",
|
||||||
|
default=None)
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
args = parseArgv(sys.argv[1:])
|
||||||
|
|
||||||
|
instrument = socket.gethostname().split('.')[0] if args.instrument == 'HOST' else args.instrument
|
||||||
|
|
||||||
|
server.run(int(args.port), SEHistory(), InfluxGraph, Client, single_instrument=instrument, secop=SecopInteractor)
|
||||||
|
165
secop.py
165
secop.py
@ -1,9 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
from base import HandlerBase
|
||||||
from base import Instrument, get_abs_time
|
from frappy.client import SecopClient
|
||||||
from frappy.client import SecopClient as SecNodeClient
|
# from frappy.lib.enum import EnumMember
|
||||||
from frappy.lib.enum import EnumMember
|
# from frappy.datatypes import get_datatype
|
||||||
from frappy.datatypes import get_datatype
|
|
||||||
|
|
||||||
|
|
||||||
def convert_par(module, name, par):
|
def convert_par(module, name, par):
|
||||||
@ -20,39 +19,38 @@ def convert_par(module, name, par):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class SecopClient:
|
class SecopInteractor(SecopClient):
|
||||||
prio_par = ["value", "status", "target"]
|
prio_par = ["value", "status", "target"]
|
||||||
hide_par = ["baseclass", "class", "pollinterval"]
|
hide_par = ["baseclass", "class", "pollinterval"]
|
||||||
skip_par = ["status2"]
|
skip_par = ["status2"]
|
||||||
|
|
||||||
def __init__(self, instrument):
|
def __init__(self, uri, node_map):
|
||||||
self.instrument = instrument
|
super().__init__(uri)
|
||||||
self.id = uuid.uuid4().hex[0:15]
|
|
||||||
self.module_updates = set()
|
self.module_updates = set()
|
||||||
self.param_updates = set()
|
self.param_updates = set()
|
||||||
self.updates = {}
|
self.updates = {}
|
||||||
|
|
||||||
def w_getblock(self, path):
|
def add_main_components(self, components):
|
||||||
path = path.split(',')[-1] # TODO: why this?
|
# todo: treat non Readable classes correctly
|
||||||
if path == "main":
|
components.extend(dict(type='rdlink', name=name + ':value', title=name)
|
||||||
components = [dict(type='rdlink', name=f'{name}:value', statusname=f'{name}:status', title=name)
|
for name in self.modules)
|
||||||
for node in self.instrument.nodes for name in node.modules]
|
self.param_updates.add('value')
|
||||||
self.param_updates = {'value', 'status'}
|
self.param_updates.add('status')
|
||||||
return dict(type='draw', path='main', title='modules', components=components)
|
|
||||||
|
def get_components(self, path):
|
||||||
|
module = self.modules[path]
|
||||||
self.module_updates.add(path) # TODO: remove others?
|
self.module_updates.add(path) # TODO: remove others?
|
||||||
node = self.instrument.node_map[path]
|
|
||||||
module = node.modules[path]
|
|
||||||
# logging.info('MP %r', path)
|
# logging.info('MP %r', path)
|
||||||
parameters = dict(module["parameters"])
|
parameters = dict(module["parameters"])
|
||||||
components = []
|
components = []
|
||||||
for name in SecopClient.skip_par:
|
for name in SecopInteractor.skip_par:
|
||||||
if name in parameters:
|
if name in parameters:
|
||||||
parameters.pop(name)
|
parameters.pop(name)
|
||||||
for name in SecopClient.prio_par:
|
for name in SecopInteractor.prio_par:
|
||||||
if name in parameters:
|
if name in parameters:
|
||||||
components.append(convert_par(path, name, parameters.pop(name)))
|
components.append(convert_par(path, name, parameters.pop(name)))
|
||||||
components1 = []
|
components1 = []
|
||||||
for name in SecopClient.hide_par:
|
for name in SecopInteractor.hide_par:
|
||||||
if name in parameters:
|
if name in parameters:
|
||||||
components1.append(convert_par(path, name, parameters.pop(name)))
|
components1.append(convert_par(path, name, parameters.pop(name)))
|
||||||
for name, p in parameters.items():
|
for name, p in parameters.items():
|
||||||
@ -72,27 +70,22 @@ class SecopClient:
|
|||||||
# print(item)
|
# print(item)
|
||||||
self.updates[key] = item
|
self.updates[key] = item
|
||||||
|
|
||||||
def w_updateblock(self, path):
|
def update_main(self):
|
||||||
if path == 'main':
|
cache = self.cache
|
||||||
path = ''
|
for modname in self.modules:
|
||||||
for node in self.instrument.nodes:
|
key = modname, 'value'
|
||||||
for modname in node.modules:
|
if key in cache:
|
||||||
key = modname, 'value'
|
self.updateItem(*key, cache[key])
|
||||||
if key in node.cache:
|
|
||||||
self.updateItem(*key, node.cache[key])
|
|
||||||
else:
|
|
||||||
node = self.instrument.node_map[path]
|
|
||||||
for param in node.modules[path]['parameters']:
|
|
||||||
key = path, param
|
|
||||||
if key in node.cache:
|
|
||||||
self.updateItem(*key, node.cache[key])
|
|
||||||
return dict(type='accept-block')
|
|
||||||
|
|
||||||
def w_console(self):
|
def update_params(self, path):
|
||||||
return dict(type='accept-console')
|
cache = self.cache
|
||||||
|
for param in self.modules[path]['parameters']:
|
||||||
|
key = path, param
|
||||||
|
if key in cache:
|
||||||
|
self.updateItem(*key, cache[key])
|
||||||
|
|
||||||
def w_sendcommand(self, command):
|
def handle_command(self, command):
|
||||||
logging.info('SENDCOMMAND %r', command)
|
"""handle command if we can, else return False"""
|
||||||
if not command.strip():
|
if not command.strip():
|
||||||
return dict(type='accept-command')
|
return dict(type='accept-command')
|
||||||
if command.startswith('change '):
|
if command.startswith('change '):
|
||||||
@ -101,62 +94,54 @@ class SecopClient:
|
|||||||
module, _, parameter = modpar.partition(':')
|
module, _, parameter = modpar.partition(':')
|
||||||
if not parameter:
|
if not parameter:
|
||||||
parameter = 'target'
|
parameter = 'target'
|
||||||
node = self.instrument.node_map[module]
|
if module not in self.modules:
|
||||||
|
return False
|
||||||
|
logging.info('SENDCOMMAND %r', command)
|
||||||
try:
|
try:
|
||||||
node.setParameterFromString(module, parameter, strvalue)
|
self.setParameterFromString(module, parameter, strvalue)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{e!r} converting {strvalue} to {node.modules[module]['parameters'][parameter]['datatype']}")
|
print(f"{e!r} converting {strvalue} to {self.modules[module]['parameters'][parameter]['datatype']}")
|
||||||
return dict(type='accept-command')
|
return True
|
||||||
|
|
||||||
def w_gettime(self, time):
|
def get_updates(self):
|
||||||
"""parse time (using server time)
|
|
||||||
time: comma separated time range (beg,end) values < 1 year are treated as relative to the current time
|
|
||||||
"""
|
|
||||||
time = [float(t) for t in time.split(',')]
|
|
||||||
return dict(type='time', time=get_abs_time(time))
|
|
||||||
|
|
||||||
def poll(self):
|
|
||||||
updates, self.updates = self.updates, {}
|
updates, self.updates = self.updates, {}
|
||||||
if not updates:
|
return list(updates.values())
|
||||||
return []
|
|
||||||
messages = [dict(type='update', updates=list(updates.values()))]
|
|
||||||
return messages
|
|
||||||
|
|
||||||
def info(self):
|
def info(self):
|
||||||
return ["na"]
|
return ["na"]
|
||||||
|
|
||||||
|
|
||||||
class SecopInstrument(Instrument):
|
# class SecopInstrument(HandlerBase):
|
||||||
|
#
|
||||||
def __init__(self, inst_name, instrument_config):
|
# def __init__(self, inst_name, instrument_config):
|
||||||
super().__init__()
|
# super().__init__()
|
||||||
self.instrument_config = instrument_config
|
# self.instrument_config = instrument_config
|
||||||
host_ports = instrument_config['hostport']
|
# host_ports = instrument_config['hostport']
|
||||||
self.logger_dir = instrument_config.get('logger_dir', '')
|
# self.logger_dir = instrument_config.get('logger_dir', '')
|
||||||
# test_day = instrument_config.get('test_day', None)
|
# # test_day = instrument_config.get('test_day', None)
|
||||||
# self.test_day = [int(x) for x in test_day.split('-')] if test_day else None
|
# # self.test_day = [int(x) for x in test_day.split('-')] if test_day else None
|
||||||
self.title = inst_name
|
# self.title = inst_name
|
||||||
self.device = ''
|
# self.device = ''
|
||||||
self.nodes = []
|
# self.nodes = []
|
||||||
self.node_map = {}
|
# self.node_map = {}
|
||||||
for host_port in host_ports.split(','):
|
# for host_port in host_ports.split(','):
|
||||||
node = SecNodeClient(host_port)
|
# node = SecopClient(host_port)
|
||||||
node.connect()
|
# node.connect()
|
||||||
self.nodes.append(node)
|
# self.nodes.append(node)
|
||||||
for name, mod in node.modules.items():
|
# for name, mod in node.modules.items():
|
||||||
self.node_map[name] = node
|
# self.node_map[name] = node
|
||||||
|
#
|
||||||
def register(self, client):
|
# def register(self, client):
|
||||||
print('OPEN')
|
# print('OPEN')
|
||||||
for node in self.nodes:
|
# for node in self.nodes:
|
||||||
node.register_callback(None, client.updateItem)
|
# node.register_callback(None, client.updateItem)
|
||||||
return super().register(client)
|
# return super().register(client)
|
||||||
|
#
|
||||||
def remove(self, client):
|
# def remove(self, client):
|
||||||
print('REMOVE')
|
# print('REMOVE')
|
||||||
for node in self.nodes:
|
# for node in self.nodes:
|
||||||
node.unregister_callback(None, client.updateItem)
|
# node.unregister_callback(None, client.updateItem)
|
||||||
super().remove(client)
|
# super().remove(client)
|
||||||
|
#
|
||||||
def new_client(self):
|
# def new_client(self):
|
||||||
return self.register(SecopClient(self))
|
# return self.register(SecopClient(self))
|
||||||
|
160
webserver.py
160
webserver.py
@ -43,7 +43,91 @@ def to_json_sse(msg):
|
|||||||
return 'data: %s\n\n' % txt
|
return 'data: %s\n\n' % txt
|
||||||
|
|
||||||
|
|
||||||
instrument = None
|
class Server:
|
||||||
|
"""singleton: created once in this module"""
|
||||||
|
interactor_classes = None
|
||||||
|
client_cls = None
|
||||||
|
history_cls = None
|
||||||
|
history = None
|
||||||
|
single_instrument = None
|
||||||
|
db = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.instruments = {}
|
||||||
|
self.clients = {}
|
||||||
|
|
||||||
|
def remove(self, client):
|
||||||
|
try:
|
||||||
|
del self.clients[client.id]
|
||||||
|
except KeyError:
|
||||||
|
logging.warning('client already removed %s', client.id)
|
||||||
|
|
||||||
|
def lookup_streams(self, instrument, stream=None, device=None):
|
||||||
|
if self.single_instrument:
|
||||||
|
instrument = self.single_instrument
|
||||||
|
if stream:
|
||||||
|
if isinstance(stream, str):
|
||||||
|
streams = stream.split(',') if stream else []
|
||||||
|
else:
|
||||||
|
streams = stream
|
||||||
|
else:
|
||||||
|
streams = []
|
||||||
|
device_names = devices = device.split(',') if device else []
|
||||||
|
tags = {}
|
||||||
|
if instrument:
|
||||||
|
# tags['instrument'] = instrument
|
||||||
|
stream_dict = self.db.get_streams(instrument, stream=list(streams), device=devices)
|
||||||
|
streams.extend((s for s in stream_dict if s not in streams))
|
||||||
|
if not devices:
|
||||||
|
device_names = list(filter(None, (t.get('device') for t in stream_dict.values())))
|
||||||
|
if streams:
|
||||||
|
tags['stream'] = streams[0] if len(streams) == 1 else streams
|
||||||
|
if devices:
|
||||||
|
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):
|
||||||
|
streams, tags, device_name = self.lookup_streams(instrument, stream, device)
|
||||||
|
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
|
||||||
|
# so there is no reference needed to history anymore
|
||||||
|
client.handlers.update(history.handlers)
|
||||||
|
self.clients[client.id] = client
|
||||||
|
return client
|
||||||
|
|
||||||
|
def run(self, port, db, history_cls, client_cls, single_instrument=None, **interactor_classes):
|
||||||
|
self.single_instrument = single_instrument
|
||||||
|
self.db = db
|
||||||
|
self.history_cls = history_cls
|
||||||
|
self.client_cls = client_cls
|
||||||
|
self.interactor_classes = interactor_classes
|
||||||
|
|
||||||
|
app.debug = True
|
||||||
|
|
||||||
|
logging.basicConfig(filename='webserver.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')
|
||||||
|
srv = gevent.pywsgi.WSGIServer(('', port), app, log=logging.getLogger('server'))
|
||||||
|
|
||||||
|
def handle_term(sig, frame):
|
||||||
|
srv.stop()
|
||||||
|
srv.close()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, handle_term)
|
||||||
|
|
||||||
|
# def handle_pdb(sig, frame):
|
||||||
|
# import pdb
|
||||||
|
# print('PDB')
|
||||||
|
# pdb.Pdb().set_trace(frame)
|
||||||
|
# signal.signal(signal.SIGUSR1, handle_pdb)
|
||||||
|
|
||||||
|
srv.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
server = Server()
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
|
|
||||||
update_rider = circularlog.Rider("upd")
|
update_rider = circularlog.Rider("upd")
|
||||||
@ -51,9 +135,11 @@ pollinterval = 0.2
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/update')
|
@app.route('/update')
|
||||||
def get_update(path=None):
|
def get_update(_=None):
|
||||||
# Client Adress: socket.getfqdn(flask.request.remote_addr)
|
# Client Adress: socket.getfqdn(flask.request.remote_addr)
|
||||||
client = instrument.new_client()
|
kwargs = {k: flask.request.values.get(k) for k in ('instrument', 'stream', 'device')}
|
||||||
|
|
||||||
|
client = server.register_client(**kwargs)
|
||||||
client.remote_info = circularlog.strtm() + " " + socket.getfqdn(flask.request.remote_addr.split(':')[-1])
|
client.remote_info = circularlog.strtm() + " " + socket.getfqdn(flask.request.remote_addr.split(':')[-1])
|
||||||
|
|
||||||
@flask.stream_with_context
|
@flask.stream_with_context
|
||||||
@ -61,7 +147,8 @@ def get_update(path=None):
|
|||||||
logging.info('UPDATE %s %s', client.id, socket.getfqdn(flask.request.remote_addr.split(':')[-1]))
|
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);
|
# msg = dict(type='id', id=client.id, title=instrument.title);
|
||||||
# yield to_json_sse(msg)
|
# yield to_json_sse(msg)
|
||||||
msg = dict(type='id', id=client.id, instrument=instrument.title, device=instrument.device)
|
msg = dict(type='id', id=client.id, instrument=kwargs.get('instrument', '<unknown>'),
|
||||||
|
device=client.device_name)
|
||||||
yield to_json_sse(msg)
|
yield to_json_sse(msg)
|
||||||
try:
|
try:
|
||||||
lastmsg = time.time()
|
lastmsg = time.time()
|
||||||
@ -87,11 +174,11 @@ def get_update(path=None):
|
|||||||
logging.info("except clause %r", repr(e))
|
logging.info("except clause %r", repr(e))
|
||||||
logging.info('CLOSED %s', client.id)
|
logging.info('CLOSED %s', client.id)
|
||||||
print('CLOSE client')
|
print('CLOSE client')
|
||||||
instrument.remove(client)
|
server.remove(client)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info('error')
|
logging.info('error')
|
||||||
logging.error('%s', traceback.format_exc())
|
logging.error('%s', traceback.format_exc())
|
||||||
instrument.remove(client)
|
server.remove(client)
|
||||||
# msg = dict(type='error',error=traceback.format_exc())
|
# msg = dict(type='error',error=traceback.format_exc())
|
||||||
# yield to_json_sse(msg)
|
# yield to_json_sse(msg)
|
||||||
|
|
||||||
@ -109,8 +196,8 @@ def dump_circular():
|
|||||||
@app.route('/clients')
|
@app.route('/clients')
|
||||||
def show_clients():
|
def show_clients():
|
||||||
result = ""
|
result = ""
|
||||||
for id in instrument.clients:
|
for id in server.clients:
|
||||||
c = instrument.clients[id]
|
c = server.clients[id]
|
||||||
result += c.remote_info + " " + "; ".join(c.info()) + "<br>"
|
result += c.remote_info + " " + "; ".join(c.info()) + "<br>"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -123,9 +210,8 @@ def export():
|
|||||||
logging.info('GET %s %s', path, repr(kwargs))
|
logging.info('GET %s %s', path, repr(kwargs))
|
||||||
try:
|
try:
|
||||||
id = kwargs.pop('id')
|
id = kwargs.pop('id')
|
||||||
print('export')
|
client = server.clients[id]
|
||||||
client = instrument.clients[id]
|
bytes = client.handlers['export'](**kwargs)
|
||||||
bytes = client.w_export(**kwargs)
|
|
||||||
return flask.send_file(
|
return flask.send_file(
|
||||||
bytes,
|
bytes,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
@ -158,8 +244,8 @@ def reply():
|
|||||||
logging.info('GET %s %r', path, kwargs)
|
logging.info('GET %s %r', path, kwargs)
|
||||||
try:
|
try:
|
||||||
id = kwargs.pop('id')
|
id = kwargs.pop('id')
|
||||||
client = instrument.clients[id]
|
client = server.clients[id]
|
||||||
msg = getattr(client, "w_" + path[1:])(**kwargs)
|
msg = client.handlers[path[1:]](**kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('%s', traceback.format_exc())
|
logging.error('%s', traceback.format_exc())
|
||||||
circularlog.log()
|
circularlog.log()
|
||||||
@ -211,6 +297,9 @@ def replace_by_empty(file):
|
|||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def default():
|
def default():
|
||||||
|
if not any(flask.request.values.get(k) for k in ('instrument', 'server', 'device')):
|
||||||
|
if not server.single_instrument:
|
||||||
|
return select_experiment()
|
||||||
return general_file('SEAWebClient.html')
|
return general_file('SEAWebClient.html')
|
||||||
|
|
||||||
|
|
||||||
@ -224,7 +313,7 @@ th {
|
|||||||
</style>
|
</style>
|
||||||
<tr><th>instrument</th><th colspan=99>devices</th></tr>''']
|
<tr><th>instrument</th><th colspan=99>devices</th></tr>''']
|
||||||
result = {}
|
result = {}
|
||||||
for stream, tags in instrument.get_streams().items():
|
for stream, tags in server.db.get_streams().items():
|
||||||
ins = tags.get('instrument', '0')
|
ins = tags.get('instrument', '0')
|
||||||
result.setdefault(ins, []).append((stream, tags.get('device')))
|
result.setdefault(ins, []).append((stream, tags.get('device')))
|
||||||
bare_streams = result.pop('0', [])
|
bare_streams = result.pop('0', [])
|
||||||
@ -240,7 +329,9 @@ th {
|
|||||||
|
|
||||||
@app.route('/select_experiment')
|
@app.route('/select_experiment')
|
||||||
def select_experiment():
|
def select_experiment():
|
||||||
out = ['''<html><body>
|
out = ['''<html><head>
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<style>
|
<style>
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -249,7 +340,8 @@ th {
|
|||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
</style><table>
|
</style></head>
|
||||||
|
<body><table>
|
||||||
''']
|
''']
|
||||||
showtitle = 0
|
showtitle = 0
|
||||||
ONEMONTH = 30 * 24 * 3600
|
ONEMONTH = 30 * 24 * 3600
|
||||||
@ -271,7 +363,7 @@ a {
|
|||||||
starttime, endtime = now - ONEMONTH, now
|
starttime, endtime = now - ONEMONTH, now
|
||||||
|
|
||||||
chunk_list = []
|
chunk_list = []
|
||||||
for key, chunk_dict in instrument.get_experiments(starttime, endtime).items():
|
for key, chunk_dict in server.db.get_experiments(starttime, endtime).items():
|
||||||
for (streams, devices), chunks in chunk_dict.items():
|
for (streams, devices), chunks in chunk_dict.items():
|
||||||
chunk_list.extend((r[1], r[0], key, devices) for r in chunks)
|
chunk_list.extend((r[1], r[0], key, devices) for r in chunks)
|
||||||
chunk_list.sort(reverse=True)
|
chunk_list.sort(reverse=True)
|
||||||
@ -322,37 +414,3 @@ def general_file(file):
|
|||||||
def hostport_split(hostport):
|
def hostport_split(hostport):
|
||||||
h = hostport.split(':')
|
h = hostport.split(':')
|
||||||
return (h[0], int(h[1]))
|
return (h[0], int(h[1]))
|
||||||
|
|
||||||
|
|
||||||
# def handle_pdb(sig, frame):
|
|
||||||
# import pdb
|
|
||||||
# print('PDB')
|
|
||||||
# pdb.Pdb().set_trace(frame)
|
|
||||||
|
|
||||||
|
|
||||||
def main(cls, **config):
|
|
||||||
global instrument
|
|
||||||
if not config:
|
|
||||||
for arg in sys.argv[1:]:
|
|
||||||
key, _, value = arg.partition('=')
|
|
||||||
config[key] = value
|
|
||||||
port = int(config['port'])
|
|
||||||
inst_name = config['instrument']
|
|
||||||
instrument = cls(inst_name, config)
|
|
||||||
|
|
||||||
# signal.signal(signal.SIGUSR1, handle_pdb)
|
|
||||||
|
|
||||||
def handle_term(sig, _):
|
|
||||||
server.stop()
|
|
||||||
server.close()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, handle_term)
|
|
||||||
|
|
||||||
app.debug = True
|
|
||||||
|
|
||||||
logging.basicConfig(filename='log/%s.log' % inst_name, filemode='w', level=logging.INFO,
|
|
||||||
format='%(asctime)s %(levelname)s %(message)s')
|
|
||||||
|
|
||||||
#server = gevent.wsgi.WSGIServer(('', port), app, keyfile='key.key', certfile='key.crt')
|
|
||||||
server = gevent.pywsgi.WSGIServer(('', port), app, log=logging.getLogger('server'))
|
|
||||||
server.serve_forever()
|
|
||||||
|
Reference in New Issue
Block a user