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:
l_samenv
2025-03-19 08:14:06 +01:00
parent b8ac8f8bb5
commit 958e472f3b
8 changed files with 445 additions and 292 deletions

69
base.py
View File

@ -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"]

View File

@ -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;

View File

@ -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

View File

@ -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) {

View File

@ -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)

View File

@ -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
View File

@ -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))

View File

@ -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()