restructure code and use frappy.client for SECoP
- separation of SECoP part from webserver - use frappy client instead of own code for SECoP communication
This commit is contained in:
36
base.py
Normal file
36
base.py
Normal file
@ -0,0 +1,36 @@
|
||||
import time
|
||||
import logging
|
||||
|
||||
|
||||
class Logger(object):
|
||||
def __init__(self, logpath):
|
||||
self.terminal = sys.stdout
|
||||
self.log = open(logpath, "a")
|
||||
|
||||
def write(self, message):
|
||||
self.terminal.write(message)
|
||||
self.log.write(message)
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
|
||||
class Instrument:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def remove(self, client):
|
||||
try:
|
||||
del self.clients[client.id]
|
||||
except KeyError:
|
||||
logging.warning('client already removed %s', client.id)
|
||||
|
||||
def register(self, client):
|
||||
self.clients[client.id] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_abs_time(times):
|
||||
now = int(time.time() + 0.999)
|
||||
oneyear = 365 * 24 * 3600
|
||||
return [t + now if t < oneyear else t for t in times]
|
@ -208,7 +208,7 @@ function updateValues(message, src) {
|
||||
let elem = matches[j];
|
||||
let type = elem.__ctype__;
|
||||
if (type == "rdonly" || type == "rdlink") {
|
||||
let text = htmlEscape(value);
|
||||
let text = htmlEscape(component.formatted);
|
||||
if (text) {
|
||||
elem.innerHTML = text;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
[chart]
|
||||
tt=unit:K
|
||||
tt.target=unit:K
|
||||
tt.set_power=unit:W
|
||||
tt.power=unit:W
|
||||
cc=-
|
||||
hemot.target=-
|
||||
mf=unit:T
|
||||
|
130
influxgraph.py
130
influxgraph.py
@ -4,24 +4,33 @@ from colors import assign_colors_to_curves
|
||||
import json
|
||||
import io
|
||||
from chart_config import ChartConfig
|
||||
from base import Instrument
|
||||
|
||||
|
||||
class InfluxGraph:
|
||||
"""
|
||||
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 :
|
||||
HISTORICAL (int) : value that represents the "historical" visualization mode, meaning that the most recent point is not in the visualisation window (no live data is sent).
|
||||
ACTUAL (int) : value that represents the "actual" visualization mode, wihch is an intermediate state used before going for live mode (the requested time window includes now)
|
||||
LIVE (int) : value that represents the "live" visualization mode, meaning that new points are sent to the client.
|
||||
HISTORICAL (int) : value that represents the "historical" visualization mode, meaning that the
|
||||
most recent point is not in the visualisation window (no live data is sent).
|
||||
ACTUAL (int) : value that represents the "actual" visualization mode, wihch is an intermediate
|
||||
state used before going for live mode (the requested time window includes now)
|
||||
LIVE (int) : value that represents the "live" visualization mode, meaning that new points are
|
||||
sent to the client.
|
||||
|
||||
Attributes :
|
||||
influx_data_getter (InfluxDataGetter) : the InfluxDataGetter instance that allows to get data out of InfluxDB.
|
||||
chart_configs ([ChartConfig]) : an array of chart configuration to apply when /getvars is called
|
||||
livemode (int) : the type of visualization the user is currently in. Can be HISTORICAL, ACTUAL or LIVE.
|
||||
end_query (int) : the unix timestamp in seconds of the most recent requested point in time of the last query or update.
|
||||
lastvalues ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values are tuples, where the first
|
||||
value is the unix timestamp of the most recent value known for this variable, and the second value its corresponding value
|
||||
variables ({(str):(str)}) : a dictionnary of the current available variables requested by the client. The key is the InfluxDB name of the curve, and the value is its label in the GUI.
|
||||
end_query (int) : the unix timestamp in seconds of the most recent requested point in time of the last query
|
||||
or update.
|
||||
lastvalues ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values
|
||||
are tuples, where the first value is the unix timestamp of the most recent value known for this variable,
|
||||
and the second value its corresponding value
|
||||
variables ({(str):(str)}) : a dictionnary of the current available variables requested by the client.
|
||||
The key is the InfluxDB name of the curve, and the value is its label in the GUI.
|
||||
"""
|
||||
HISTORICAL = 0
|
||||
ACTUAL = 1
|
||||
@ -33,13 +42,12 @@ class InfluxGraph:
|
||||
self.livemode = self.HISTORICAL
|
||||
self.end_query = 0
|
||||
self.lastvalues = {}
|
||||
self.variables = {} # name:label
|
||||
|
||||
self.variables = {} # name:label
|
||||
|
||||
def get_abs_time(self, times):
|
||||
"""
|
||||
Gets the absolute times for the given pontential relative times. If the given timestamps are less than one year, then the value is relative
|
||||
and converted into an asbolute timestamps
|
||||
Gets the absolute times for the given potential relative times. If a given timestamp is less than
|
||||
one year, then the value is relative and converted into an absolute timestamp
|
||||
|
||||
Parameters :
|
||||
times([(float)]) : an array of unix timestamps or relative duration (< 1 year) as floats
|
||||
@ -58,7 +66,7 @@ class InfluxGraph:
|
||||
|
||||
Parameters :
|
||||
result ({(str):[[(int),(float)]]}) : a dictionnary with the variable names as key, and an array of points,
|
||||
which are an array containing the timestamp as their first value, and the y-value in float as their second one.
|
||||
which are a tuple (timestamp, y-value as float)
|
||||
endtime (int) : the unix timestamp in seconds of the time we want to have data until
|
||||
"""
|
||||
for var, c in result.items():
|
||||
@ -75,12 +83,14 @@ class InfluxGraph:
|
||||
|
||||
Parameters :
|
||||
variables (str) : a comma separataed value string of variable names (influx names) to retrieve
|
||||
time (str) : a commma separated value string (range) of seconds. They are treated as relative from now if they are lesser than one year.
|
||||
time (str) : a commma separated value string (range) of seconds. They are treated as relative from now
|
||||
if they are lesser than one year.
|
||||
interval (str) : the interval (resolution) of the values to get (string in milliseconds)
|
||||
|
||||
Returns :
|
||||
{"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionnary with its "graph-draw" type (so it can be processed by the client), and a "graph" dictionnary with the variable names as key, and an array of points,
|
||||
which are an array containing the timestamp as their first value, and the y-value in float as their second one.
|
||||
{"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionnary with its "graph-draw" type
|
||||
(so it can be processed by the client), and a "graph" dictionnary with the variable names as key,
|
||||
and an array of points as a tuple (timestamp, y-value as float)
|
||||
"""
|
||||
time = [float(t) for t in time.split(',')]
|
||||
start, end, now = self.get_abs_time(time + [0])
|
||||
@ -103,11 +113,12 @@ class InfluxGraph:
|
||||
Called when the route /gettime is reached.
|
||||
|
||||
Parameters :
|
||||
time (str="-1800,0") : the given point in time represented by a string, which is a comma separated unix timestamp values list (in seconds). They are treated as relative from now if they are lesser than one year.
|
||||
time (str="-1800,0") : the given point in time represented by a string, which is a comma separated unix
|
||||
timestamp values list (in seconds). They are treated as relative from now if they are lesser than one year.
|
||||
|
||||
Returns :
|
||||
{"type":"time", "time":(int)} : a dictionnary with its "time" type (so the data can be processed by the client) and the server unix timestamp in seconds corresponding
|
||||
to the time asked by the client
|
||||
{"type":"time", "time":(int)} : a dictionnary with its "time" type (so the data can be processed by the
|
||||
client) and the server unix timestamp in seconds corresponding to the time asked by the client
|
||||
"""
|
||||
time = [float(t) for t in time.split(',')]
|
||||
return dict(type='time', time= self.get_abs_time(time))
|
||||
@ -118,13 +129,17 @@ class InfluxGraph:
|
||||
Called when the route /getvars is reached.
|
||||
|
||||
Parameters :
|
||||
time (str) : the given point in time represented by a string, which is a unix timestamp in seconds. It is treated as relative from now if it is lesser than one year.
|
||||
time (str) : the given point in time represented by a string, which is a unix timestamp in seconds.
|
||||
It is treated as relative from now if it is lesser than one year.
|
||||
userconfiguration (str|None) : the JSON string representing the user configuration
|
||||
|
||||
Returns :
|
||||
{"type":"var_list", "device":(str), "blocks":[{"tag":(str),"unit":(str), "curves":[{"name":(str), "label":(str), "color":(str), "original_color":(str)}]}]} :
|
||||
a dictionnary with its "var_list" type (so the data can be processed by the client), the device that was currently set at that time, and the available curves with the name of the internal variable,
|
||||
the color to display for this curve, its original color in SEA, grouped by their tag (which is a category or unit if absent) and their unit (in "blocks")
|
||||
{"type":"var_list", "device":(str), "blocks":[{"tag":(str),"unit":(str), "curves":
|
||||
[{"name":(str), "label":(str), "color":(str), "original_color":(str)}]}]}:
|
||||
a dictionnary with its "var_list" type (so the data can be processed by the client), the device that
|
||||
was currently set at that time, and the available curves with the name of the internal variable,
|
||||
the color to display for this curve, its original color in SEA, grouped by their tag (which is a
|
||||
category or unit if absent) and their unit (in "blocks")
|
||||
"""
|
||||
|
||||
time = [float(t) for t in time.split(',')]
|
||||
@ -148,7 +163,8 @@ class InfluxGraph:
|
||||
Sets the current visualisation mode to LIVE if not in HISTORICAL mode.
|
||||
Called when the route /updategraph is reached.
|
||||
Returns :
|
||||
{"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live" value telling if the server could change its visualization mode to live
|
||||
{"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live"
|
||||
value telling if the server could change its visualization mode to live
|
||||
"""
|
||||
logging.info("UPD GRAPH %d", self.livemode)
|
||||
if self.livemode == self.HISTORICAL:
|
||||
@ -188,12 +204,16 @@ class InfluxGraph:
|
||||
|
||||
def graphpoll(self):
|
||||
"""
|
||||
Polls the last known values for all the available variables, and returns only those whose polled values are more recent than the most recent displayed one.
|
||||
Every plain minute, all the variables are returned with a point having their last known value at the current timestamp to synchronize all the curves on the GUI.
|
||||
Polls the last known values for all the available variables, and returns only those whose polled values
|
||||
are more recent than the most recent displayed one.
|
||||
Every plain minute, all the variables are returned with a point having their last known value at the current
|
||||
timestamp to synchronize all the curves on the GUI.
|
||||
|
||||
Returns :
|
||||
{"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None : a dictionnary with its "graph-update" type
|
||||
(so it can be processed by the client), and a "graph" dictionnary with the variable names as key, and an array of points, which are an array containing the timestamp
|
||||
{"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None :
|
||||
a dictionnary with its "graph-update" type
|
||||
(so it can be processed by the client), and a "graph" dictionnary with the variable names as key,
|
||||
and an array of points, which are an array containing the timestamp
|
||||
as their first value, and the y-value in float as their second one
|
||||
"""
|
||||
if self.livemode != self.LIVE:
|
||||
@ -203,7 +223,8 @@ class InfluxGraph:
|
||||
result = self.influx_data_getter.poll_last_values(list(self.variables.keys()), self.lastvalues, now)
|
||||
for variable, values in list(result.items()):
|
||||
tlast = self.lastvalues.get(variable, (0,))[0]
|
||||
# removes points older than the last known point (queries are in seconds and might return points already displayed)
|
||||
# removes points older than the last known point
|
||||
# (queries are in seconds and might return points already displayed)
|
||||
while values and values[0][0] <= tlast:
|
||||
values.pop(0)
|
||||
if values and values[-1][0] > tlast:
|
||||
@ -219,3 +240,54 @@ class InfluxGraph:
|
||||
if len(result) > 0:
|
||||
return dict(type='graph-update', time=now, graph=result)
|
||||
return None
|
||||
|
||||
|
||||
class InfluxInstrument(Instrument):
|
||||
|
||||
def __init__(self, instr_name, inst_config=None):
|
||||
self.db = InfluxDB()
|
||||
self.influx_data_getter = InfluxDataGetter(self.db, inst_name)
|
||||
self.clients = {}
|
||||
self.title = instr_name
|
||||
self.device = self.influx_data_getter.get_device_name(int(datetime.now().timestamp()))
|
||||
|
||||
def new_client(self):
|
||||
return self.register(InfluxClient())
|
||||
|
||||
|
||||
class InfluxParams:
|
||||
"""Class with dummy routes, in case client side is started with the right part init commands"""
|
||||
def __init__(self):
|
||||
self.id = uuid.uuid4().hex[0:15]
|
||||
self.queue = []
|
||||
|
||||
def poll(self):
|
||||
messages = self.queue
|
||||
self.queue = []
|
||||
msg = self.graphpoll()
|
||||
if msg:
|
||||
messages.append(msg)
|
||||
return messages
|
||||
|
||||
def info(self):
|
||||
return ["na"]
|
||||
|
||||
def w_getblock(self, path):
|
||||
return dict(type='draw', title="graph", path=path, components=[])
|
||||
|
||||
def w_updateblock(self, path):
|
||||
return dict(type='accept-block')
|
||||
|
||||
def w_console(self):
|
||||
return dict(type='accept-console')
|
||||
|
||||
def w_sendcommand(self, command):
|
||||
return dict(type='accept-command')
|
||||
|
||||
|
||||
class InfluxClient(InfluxParams, InfluxGraph):
|
||||
def __init__(self):
|
||||
InfluxParams.__init__(self)
|
||||
InfluxGraph.__init__(self, instrument.influx_data_getter, instrument.title)
|
||||
|
||||
|
||||
|
230
seagraph.py
230
seagraph.py
@ -1,230 +0,0 @@
|
||||
from datetime import date
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
class PrettyFloat(float):
|
||||
"""saves bandwidth when converting to JSON
|
||||
|
||||
a lot of numbers originally have a fixed (low) number of decimal digits
|
||||
as the binary representation is not exact, it might happen, that a
|
||||
lot of superfluous digits are transmitted:
|
||||
|
||||
str(1/10*3) == '0.30000000000000004'
|
||||
str(PrettyFloat(1/10*3)) == '0.3'
|
||||
"""
|
||||
def __repr__(self):
|
||||
return '%.15g' % self
|
||||
|
||||
#encode = "!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
||||
|
||||
def get_abs_time(times):
|
||||
now = int(time.time() + 0.999)
|
||||
oneyear = 365 * 24 * 3600
|
||||
return [t + now if t < oneyear else t for t in times]
|
||||
|
||||
class Scanner(object):
|
||||
def __init__(self, directory, test_day=None):
|
||||
self.directory = directory
|
||||
self.last_time = {}
|
||||
self.test_day = test_day
|
||||
|
||||
def scan(self, variable, timerange, result):
|
||||
"""return value is true when there are aditionnal points after the time range"""
|
||||
start, to, now = get_abs_time(timerange + [0])
|
||||
old = None
|
||||
t = 0
|
||||
for di in range(date.fromtimestamp(start).toordinal(), date.fromtimestamp(to).toordinal() + 1):
|
||||
d = date.fromordinal(di)
|
||||
year, mon, day = self.test_day if self.test_day else (d.year, d.month, d.day)
|
||||
path = self.directory + "logger/%d/%s/%.2d-%.2d.log" % \
|
||||
(year, variable.lower(), mon, day)
|
||||
try:
|
||||
# logging.info("logger path %s", path)
|
||||
with open(path) as f:
|
||||
t0 = time.mktime((d.year, d.month, d.day, 0, 0, 0, 0, 0, -1))
|
||||
for line in f:
|
||||
if line[0] != '#':
|
||||
t = t0 + (int(line[0:2]) * 60 + int(line[3:5])) * 60 + int(line[6:8])
|
||||
if line[-1:] == '\n':
|
||||
value = line[9:-1]
|
||||
else:
|
||||
value = line[9:]
|
||||
if t < start:
|
||||
old = value
|
||||
else:
|
||||
if old is not None:
|
||||
self.next(start, old, result)
|
||||
old = None
|
||||
self.next(t, value, result)
|
||||
if t > to:
|
||||
break
|
||||
except IOError:
|
||||
# print(f'error reading {path}')
|
||||
pass
|
||||
if t < start:
|
||||
#if t == 0:
|
||||
# t = start
|
||||
if old is not None:
|
||||
self.next(t, old, result)
|
||||
if t != self.last_time.get(variable,0):
|
||||
self.last_time[variable] = t
|
||||
return True
|
||||
return False
|
||||
|
||||
class NumericScanner(Scanner):
|
||||
def __init__(self, *args, **kwargs):
|
||||
Scanner.__init__(self, *args, **kwargs)
|
||||
|
||||
def next(self, t, value, result):
|
||||
try:
|
||||
value = PrettyFloat(value)
|
||||
except:
|
||||
value = None
|
||||
result.append([PrettyFloat(t), value])
|
||||
#self.value = value
|
||||
#self.last = t
|
||||
|
||||
def get_message(self, variables, timerange, show_empty=True):
|
||||
self.dirty = False
|
||||
result = {}
|
||||
for var in variables:
|
||||
self.last = 0
|
||||
curve = []
|
||||
if self.scan(var, timerange, curve):
|
||||
self.dirty = True
|
||||
if show_empty or len(curve) > 1:
|
||||
result[var] = curve
|
||||
return result
|
||||
|
||||
class ColorMap(object):
|
||||
'''
|
||||
ColorMap is using official CSS color names, with the exception of Green, as this
|
||||
is defined differently with X11 colors than in SEA, and used heavily in config files.
|
||||
Here Green is an alias to Lime (#00FF00) and MidGreen is #008000, which is called Green in CSS.
|
||||
The function to_code is case insensitive and accepts also names with underscores.
|
||||
The order is choosen by M. Zolliker for the SEA client, originally only the first 16 were used.
|
||||
'''
|
||||
hex_name = (("#FFFFFF","White"), ("#FF0000","Red"), ("#00FF00","Lime"), ("#0000FF","Blue"), ("#FF00FF","Magenta"),
|
||||
("#FFFF00","Yellow"), ("#00FFFF","Cyan"), ("#000000","Black"), ("#FFA500","Orange"), ("#006400","DarkGreen"),
|
||||
("#9400D3","DarkViolet"), ("#A52A2A","Brown"), ("#87CEEB","SkyBlue"), ("#808080","Gray"), ("#FF69B4","HotPink"),
|
||||
("#FFFFE0","LightYellow"), ("#00FF7F","SpringGreen"), ("#000080","Navy"), ("#1E90FF","DodgerBlue"),
|
||||
("#9ACD32","YellowGreen"), ("#008B8B","DarkCyan"), ("#808000","Olive"), ("#DEB887","BurlyWood"),
|
||||
("#7B68EE","MediumSlateBlue"), ("#483D8B","DarkSlateBlue"), ("#98FB98","PaleGreen"), ("#FF1493","DeepPink"),
|
||||
("#FF6347","Tomato"), ("#32CD32","LimeGreen"), ("#DDA0DD","Plum"), ("#7FFF00","Chartreuse"), ("#800080","Purple"),
|
||||
("#00CED1","DarkTurquoise"), ("#8FBC8F","DarkSeaGreen"), ("#4682B4","SteelBlue"), ("#800000","Maroon"),
|
||||
("#3CB371","MediumSeaGreen"), ("#FF4500","OrangeRed"), ("#BA55D3","MediumOrchid"), ("#2F4F4F","DarkSlateGray"),
|
||||
("#CD853F","Peru"), ("#228B22","ForestGreen"), ("#48D1CC","MediumTurquoise"), ("#DC143C","Crimson"),
|
||||
("#D3D3D3","LightGray"), ("#ADFF2F","GreenYellow"), ("#7FFFD4","Aquamarine"), ("#BC8F8F","RosyBrown"),
|
||||
("#20B2AA","LightSeaGreen"), ("#C71585","MediumVioletRed"), ("#F0E68C","Khaki"), ("#6495ED","CornflowerBlue"),
|
||||
("#556B2F","DarkOliveGreen"), ("#CD5C5C","IndianRed "), ("#2E8B57","SeaGreen"), ("#F08080","LightCoral"),
|
||||
("#8A2BE2","BlueViolet"), ("#AFEEEE","PaleTurquoise"), ("#4169E1","RoyalBlue"), ("#0000CD","MediumBlue"),
|
||||
("#B8860B","DarkGoldenRod"), ("#00BFFF","DeepSkyBlue"), ("#FFC0CB","Pink"), ("#4B0082","Indigo "), ("#A0522D","Sienna"),
|
||||
("#FFD700","Gold"), ("#F4A460","SandyBrown"), ("#DAA520","GoldenRod"), ("#DA70D6","Orchid"), ("#E6E6FA","Lavender"),
|
||||
("#5F9EA0","CadetBlue"), ("#D2691E","Chocolate"), ("#66CDAA","MediumAquaMarine"), ("#6B8E23","OliveDrab"),
|
||||
("#A9A9A9","DarkGray"), ("#BDB76B","DarkKhaki"), ("#696969","DimGray"), ("#B0C4DE","LightSteelBlue"),
|
||||
("#191970","MidnightBlue"), ("#FFE4C4","Bisque"), ("#6A5ACD","SlateBlue"), ("#EE82EE","Violet"),
|
||||
("#8B4513","SaddleBrown"), ("#FF7F50","Coral"), ("#008000","MidGreen"), ("#DB7093","PaleVioletRed"), ("#C0C0C0","Silver"),
|
||||
("#E0FFFF","LightCyan"), ("#9370DB","MediumPurple"), ("#FF8C00","DarkOrange"), ("#00FA9A","MediumSpringGreen"),
|
||||
("#E9967A","DarkSalmon"), ("#778899","LightSlateGray"), ("#9932CC","DarkOrchid"), ("#EEE8AA","PaleGoldenRod"),
|
||||
("#F8F8FF","GhostWhite"), ("#FFA07A","LightSalmon"), ("#ADD8E6","LightBlue"), ("#D8BFD8","Thistle"),
|
||||
("#FFE4E1","MistyRose"), ("#FFDEAD","NavajoWhite"), ("#40E0D0","Turquoise"), ("#90EE90","LightGreen"),
|
||||
("#B22222","FireBrick"), ("#008080","Teal"), ("#F0FFF0","HoneyDew"), ("#FFFACD","LemonChiffon"), ("#FFF5EE","SeaShell"),
|
||||
("#F5F5DC","Beige"), ("#DCDCDC","Gainsboro"), ("#FA8072","Salmon"), ("#8B008B","DarkMagenta"), ("#FFB6C1","LightPink"),
|
||||
("#708090","SlateGray"), ("#87CEFA","LightSkyBlue"), ("#FFEFD5","PapayaWhip"), ("#D2B48C","Tan"), ("#FFFFF0","Ivory"),
|
||||
("#F0FFFF","Azure"), ("#F5DEB3","Wheat"), ("#00008B","DarkBlue"), ("#FFDAB9","PeachPuff"), ("#8B0000","DarkRed"),
|
||||
("#FAF0E6","Linen"), ("#B0E0E6","PowderBlue"), ("#FFE4B5","Moccasin"), ("#F5F5F5","WhiteSmoke"), ("#FFF8DC","Cornsilk"),
|
||||
("#FFFAFA","Snow"), ("#FFF0F5","LavenderBlush"), ("#FFEBCD","BlanchedAlmond"), ("#F0F8FF","AliceBlue"),
|
||||
("#FAEBD7","AntiqueWhite"), ("#FDF5E6","OldLace"), ("#FAFAD2","LightGoldenRodYellow"), ("#F5FFFA","MintCream"),
|
||||
("#FFFAF0","FloralWhite"), ("#7CFC00","LawnGreen"), ("#663399","RebeccaPurple"))
|
||||
codes = {}
|
||||
for i, pair in enumerate(hex_name):
|
||||
codes[pair[0]] = i
|
||||
low = pair[1].lower()
|
||||
codes[low] = i
|
||||
codes[low.replace("gray","grey")] = i
|
||||
codes["green"] = 2
|
||||
codes["fuchsia"] = 4
|
||||
codes["aqua"] = 6
|
||||
|
||||
@staticmethod
|
||||
def to_code(colortext):
|
||||
try:
|
||||
return int(colortext)
|
||||
except ValueError:
|
||||
return ColorMap.codes.get(colortext.lower().replace("_",""),-1)
|
||||
|
||||
@staticmethod
|
||||
def check_hex(code):
|
||||
if not code.startswith("#"):
|
||||
return None
|
||||
if len(code) == 4: # convert short code to long code
|
||||
code = code[0:2] + code[1:3] + code[2:4] + code[3]
|
||||
if len(code) != 7:
|
||||
return None
|
||||
try:
|
||||
int(code[1:]) # we have a valid hex color code
|
||||
return code
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def to_hex(code):
|
||||
try:
|
||||
return ColorMap.hex_name[code][0]
|
||||
except IndexError:
|
||||
return -1
|
||||
|
||||
class VarsScanner(Scanner):
|
||||
colors = {"red":0}
|
||||
def __init__(self, directory, test_day=None):
|
||||
Scanner.__init__(self, directory, test_day=test_day)
|
||||
logging.info('vars dir %s', directory)
|
||||
|
||||
def next(self, t, value, result):
|
||||
logging.info('vars %s', value)
|
||||
for var in value.strip().split(" "):
|
||||
vars = var.split("|")
|
||||
if len(vars) == 1:
|
||||
vars.append("")
|
||||
if len(vars) == 2:
|
||||
vars.append(vars[0])
|
||||
if len(vars) == 3:
|
||||
vars.append("")
|
||||
name, unit, label, color = vars
|
||||
if not unit in result:
|
||||
result[unit] = dict(tag = unit, unit = unit.split("_")[0], curves=[])
|
||||
result[unit]["curves"].append(dict(name=name, label=label, color=color))
|
||||
|
||||
def get_message(self, time):
|
||||
# get last value only
|
||||
result = {}
|
||||
self.scan("vars", [time, time], result)
|
||||
for unit in result:
|
||||
color_set = set()
|
||||
auto_curves = []
|
||||
for curve in result[unit]["curves"]:
|
||||
col = curve["color"].strip()
|
||||
c = ColorMap.to_code(col)
|
||||
if c < 0:
|
||||
valid = ColorMap.check_hex(col)
|
||||
if valid:
|
||||
curve["original_color"] = col
|
||||
curve["color"] = valid
|
||||
else:
|
||||
auto_curves.append(curve)
|
||||
curve["original_color"] = col + "?"
|
||||
else:
|
||||
color_set.add(c)
|
||||
curve["original_color"] = col
|
||||
curve["color"] = ColorMap.to_hex(c)
|
||||
c = 1 # omit white
|
||||
for curve in auto_curves:
|
||||
while c in color_set: c += 1 # find unused color
|
||||
curve["color"] = ColorMap.to_hex(c)
|
||||
c += 1
|
||||
return result
|
||||
|
189
secop.py
Normal file
189
secop.py
Normal file
@ -0,0 +1,189 @@
|
||||
import time
|
||||
import logging
|
||||
import uuid
|
||||
from base import Instrument, get_abs_time
|
||||
from influxdb import InfluxDB, InfluxDataGetter
|
||||
from influxgraph import InfluxGraph
|
||||
from frappy.client import SecopClient as SecNodeClient
|
||||
from frappy.lib.enum import EnumMember
|
||||
from frappy.datatypes import get_datatype
|
||||
|
||||
|
||||
def convert_par(module, name, par):
|
||||
result = dict(type='input', name=module+":"+name, title=name)
|
||||
if par.get('readonly', True):
|
||||
result['type'] = 'rdonly'
|
||||
else:
|
||||
result['command'] = 'change %s:%s' % (module, name)
|
||||
if par['datainfo']['type'] == 'enum':
|
||||
result['enum_names'] = [dict(title=k, value=v) for k, v in par['datainfo']['members'].items()]
|
||||
result['type'] = 'enum'
|
||||
elif par['datainfo']['type'] == 'bool':
|
||||
result['type'] = 'checkbox'
|
||||
return result
|
||||
|
||||
|
||||
class SecopClient:
|
||||
prio_par = ["value", "status", "target"]
|
||||
hide_par = ["baseclass", "class", "pollinterval"]
|
||||
skip_par = ["status2"]
|
||||
|
||||
def __init__(self, instrument):
|
||||
self.instrument = instrument
|
||||
self.id = uuid.uuid4().hex[0:15]
|
||||
self.module_updates = set()
|
||||
self.param_updates = set()
|
||||
self.updates = {}
|
||||
|
||||
def w_getblock(self, path):
|
||||
path = path.split(',')[-1] # TODO: why this?
|
||||
if path == "main":
|
||||
components = [dict(type='rdlink', name=name+':value', title=name)
|
||||
for node in self.instrument.nodes for name in node.modules]
|
||||
self.param_updates = {'value'}
|
||||
return dict(type='draw', path='main', title='modules', components=components)
|
||||
self.module_updates.add(path) # TODO: remove others?
|
||||
node = self.instrument.node_map[path]
|
||||
module = node.modules[path]
|
||||
# logging.info('MP %r', path)
|
||||
parameters = dict(module["parameters"])
|
||||
components = []
|
||||
for name in SecopClient.skip_par:
|
||||
if name in parameters:
|
||||
parameters.pop(name)
|
||||
for name in SecopClient.prio_par:
|
||||
if name in parameters:
|
||||
components.append(convert_par(path, name, parameters.pop(name)))
|
||||
components1 = []
|
||||
for name in SecopClient.hide_par:
|
||||
if name in parameters:
|
||||
components1.append(convert_par(path, name, parameters.pop(name)))
|
||||
for name, p in parameters.items():
|
||||
components.append(convert_par(path, name, parameters[name]))
|
||||
components.extend(components1)
|
||||
return dict(type='draw', path=path, title=path, components=components)
|
||||
|
||||
def updateItem(self, module, parameter, entry):
|
||||
key = module, parameter
|
||||
# print(key, entry)
|
||||
if module in self.module_updates or parameter in self.param_updates:
|
||||
name = f'{module}:{parameter}'
|
||||
if entry.readerror:
|
||||
item = {'name': name, 'error': str(entry.readerror)}
|
||||
else:
|
||||
item = {'name': name, 'value': str(entry), 'formatted': entry.formatted()}
|
||||
# print(item)
|
||||
self.updates[key] = item
|
||||
|
||||
def w_updateblock(self, path):
|
||||
if path == 'main':
|
||||
path = ''
|
||||
for node in self.instrument.nodes:
|
||||
for modname in node.modules:
|
||||
key = modname, 'value'
|
||||
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):
|
||||
return dict(type='accept-console')
|
||||
|
||||
def w_sendcommand(self, command):
|
||||
logging.info('SENDCOMMAND %r', command)
|
||||
if not command.strip():
|
||||
return dict(type='accept-command')
|
||||
scmd = command.split(' ')
|
||||
if scmd[0] != 'change':
|
||||
scmd.insert(0, 'change')
|
||||
# return dict(type='accept-command', error=f'do not know how to do {command!r}')
|
||||
module, _, parameter = scmd[1].partition(':')
|
||||
if not parameter:
|
||||
parameter = 'target'
|
||||
node = self.instrument.node_map[module]
|
||||
result = node.setParameterFromString(module, parameter, scmd[2])
|
||||
return dict(type='accept-command')
|
||||
|
||||
def w_gettime(self, time):
|
||||
"""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, {}
|
||||
if not updates:
|
||||
return []
|
||||
messages = [dict(type='update', updates=list(updates.values()))]
|
||||
return messages
|
||||
|
||||
def info(self):
|
||||
return ["na"]
|
||||
|
||||
|
||||
class SecopInstrument(Instrument):
|
||||
|
||||
def __init__(self, inst_name, instrument_config):
|
||||
super().__init__()
|
||||
self.instrument_config = instrument_config
|
||||
host_ports = instrument_config['hostport']
|
||||
self.logger_dir = instrument_config.get('logger_dir', '')
|
||||
# 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.title = inst_name
|
||||
self.device = 'UNDEFINED'
|
||||
self.clients = {}
|
||||
self.nodes = []
|
||||
self.node_map = {}
|
||||
for host_port in host_ports.split(','):
|
||||
node = SecNodeClient(host_port)
|
||||
node.connect()
|
||||
self.nodes.append(node)
|
||||
for name, mod in node.modules.items():
|
||||
self.node_map[name] = node
|
||||
|
||||
def register(self, client):
|
||||
print('OPEN')
|
||||
for node in self.nodes:
|
||||
node.register_callback(None, client.updateItem)
|
||||
return super().register(client)
|
||||
|
||||
def remove(self, client):
|
||||
print('REMOVE')
|
||||
for node in self.nodes:
|
||||
node.unregister_callback(None, client.updateItem)
|
||||
super().remove(client)
|
||||
|
||||
def new_client(self):
|
||||
return self.register(SecopClient(self))
|
||||
|
||||
|
||||
class SecopInfluxClient(SecopClient, InfluxGraph):
|
||||
def __init__(self, instrument):
|
||||
SecopClient.__init__(self, instrument)
|
||||
InfluxGraph.__init__(self, instrument.influx_data_getter, instrument.title)
|
||||
|
||||
def poll(self):
|
||||
messages = super().poll()
|
||||
msg = self.graphpoll()
|
||||
if msg:
|
||||
messages.append(msg)
|
||||
return messages
|
||||
|
||||
|
||||
class SecopInfluxInstrument(SecopInstrument):
|
||||
|
||||
def __init__(self, inst_name, instrument_config):
|
||||
super().__init__(inst_name, instrument_config)
|
||||
self.db = InfluxDB()
|
||||
self.influx_data_getter = InfluxDataGetter(self.db, inst_name)
|
||||
self.device = self.influx_data_getter.get_device_name(int(time.time()))
|
||||
|
||||
def new_client(self):
|
||||
return self.register(SecopInfluxClient(self))
|
268
webserver.py
Executable file
268
webserver.py
Executable file
@ -0,0 +1,268 @@
|
||||
from gevent import monkey
|
||||
monkey.patch_all()
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import traceback
|
||||
import logging
|
||||
import json
|
||||
from collections import deque
|
||||
from datetime import date, datetime
|
||||
import tcp_lineserver
|
||||
import gevent
|
||||
import gevent.pywsgi
|
||||
import gevent.queue
|
||||
from gevent.lock import RLock
|
||||
import flask
|
||||
import pprint
|
||||
import random
|
||||
import uuid
|
||||
import circularlog
|
||||
|
||||
|
||||
def guess_mimetype(filename):
|
||||
if filename.endswith('.js'):
|
||||
mimetype = 'text/javascript'
|
||||
elif filename.endswith('.css'):
|
||||
mimetype = 'text/css'
|
||||
elif filename.endswith('.ico'):
|
||||
mimetype = 'image/x-icon'
|
||||
elif filename.endswith(".png"):
|
||||
mimetype = "image/png"
|
||||
else:
|
||||
mimetype = 'text/html'
|
||||
return mimetype
|
||||
|
||||
|
||||
class MyEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
try:
|
||||
return super().default(obj)
|
||||
except TypeError:
|
||||
return int(obj) # try to convert SECoP Enum
|
||||
|
||||
|
||||
# SSE 'protocol' is described here: https://bit.ly/UPFyxY
|
||||
def to_json_sse(msg):
|
||||
txt = json.dumps(msg, separators=(',', ': '), cls=MyEncoder)
|
||||
logging.debug('data: %s', txt)
|
||||
return 'data: %s\n\n' % txt
|
||||
|
||||
|
||||
instrument = None
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
update_rider = circularlog.Rider("upd")
|
||||
pollinterval = 0.2
|
||||
|
||||
|
||||
@app.route('/update')
|
||||
def get_update(path=None):
|
||||
# Client Adress: socket.getfqdn(flask.request.remote_addr)
|
||||
client = instrument.new_client()
|
||||
client.remote_info = circularlog.strtm() + " " + socket.getfqdn(flask.request.remote_addr.split(':')[-1])
|
||||
|
||||
@flask.stream_with_context
|
||||
def generator():
|
||||
logging.info('UPDATE %s %s', client.id, socket.getfqdn(flask.request.remote_addr.split(':')[-1]))
|
||||
# msg = dict(type='id', id=client.id, title=instrument.title);
|
||||
# yield to_json_sse(msg)
|
||||
msg = dict(type='id', id=client.id, instrument=instrument.title, device=instrument.device)
|
||||
yield to_json_sse(msg)
|
||||
try:
|
||||
lastmsg = time.time()
|
||||
while True:
|
||||
if client.info() == "":
|
||||
print(time.time()-lastmsg)
|
||||
messages = client.poll()
|
||||
for msg in messages:
|
||||
update_rider.put('-', repr(msg))
|
||||
yield to_json_sse(msg)
|
||||
if messages:
|
||||
lastmsg = time.time()
|
||||
else:
|
||||
if time.time() > lastmsg + 30:
|
||||
if not client.info():
|
||||
raise GeneratorExit("no activity")
|
||||
logging.info('HEARTBEAT %s (%s)', client.id, "; ".join(client.info()))
|
||||
yield to_json_sse(dict(type='heartbeat'))
|
||||
lastmsg = time.time()
|
||||
else:
|
||||
gevent.sleep(pollinterval)
|
||||
except (GeneratorExit, tcp_lineserver.Disconnected) as e:
|
||||
logging.info("except clause %r", repr(e))
|
||||
logging.info('CLOSED %s', client.id)
|
||||
print('CLOSE client')
|
||||
instrument.remove(client)
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.info('error')
|
||||
logging.error('%s', traceback.format_exc())
|
||||
instrument.remove(client)
|
||||
# msg = dict(type='error',error=traceback.format_exc())
|
||||
# yield to_json_sse(msg)
|
||||
|
||||
resp = flask.Response(generator(), mimetype='text/event-stream')
|
||||
resp.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/circular')
|
||||
def dump_circular():
|
||||
circularlog.log()
|
||||
return "log"
|
||||
|
||||
|
||||
@app.route('/clients')
|
||||
def show_clients():
|
||||
result = ""
|
||||
for id in instrument.clients:
|
||||
c = instrument.clients[id]
|
||||
result += c.remote_info + " " + "; ".join(c.info()) + "<br>"
|
||||
return result
|
||||
|
||||
|
||||
@app.route('/export')
|
||||
def export():
|
||||
args = flask.request.args
|
||||
kwargs = dict((k, args.get(k)) for k in args)
|
||||
path = flask.request.path
|
||||
logging.info('GET %s %s', path, repr(kwargs))
|
||||
try:
|
||||
id = kwargs.pop('id')
|
||||
client = instrument.clients[id]
|
||||
bytes = client.w_export(**kwargs)
|
||||
return flask.send_file(
|
||||
bytes,
|
||||
as_attachment=True,
|
||||
download_name='export.tsv',
|
||||
mimetype='text/tab-separated-values'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error('%s', traceback.format_exc())
|
||||
circularlog.log()
|
||||
msg = dict(type='error', request=path[1:], error=repr(e))
|
||||
logging.error('MSG: %r', msg)
|
||||
resp = flask.Response(json.dumps(msg), mimetype='application/json')
|
||||
resp.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/getblock')
|
||||
@app.route('/updateblock')
|
||||
@app.route('/sendcommand')
|
||||
@app.route('/console')
|
||||
@app.route('/graph')
|
||||
@app.route('/updategraph')
|
||||
@app.route('/gettime')
|
||||
@app.route('/getvars', methods=["GET", "POST"])
|
||||
def reply():
|
||||
args = flask.request.values
|
||||
kwargs = dict((k, args.get(k)) for k in args)
|
||||
path = flask.request.path
|
||||
logging.info('GET %s %r', path, kwargs)
|
||||
try:
|
||||
id = kwargs.pop('id')
|
||||
client = instrument.clients[id]
|
||||
msg = getattr(client, "w_" + path[1:])(**kwargs)
|
||||
except Exception as e:
|
||||
logging.error('%s', traceback.format_exc())
|
||||
circularlog.log()
|
||||
msg = dict(type='error', request=path[1:], error=repr(e))
|
||||
logging.info('REPLY %s %r', path, msg)
|
||||
resp = flask.Response(json.dumps(msg), mimetype='application/json')
|
||||
resp.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/test/<file>')
|
||||
def subdir_test_file(file):
|
||||
gevent.sleep(2)
|
||||
resp = flask.send_file("client/test/"+file, mimetype=guess_mimetype(file))
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/components/curves_settings_popup/color_selector/<file>')
|
||||
@app.route('/components/curves_settings_popup/<file>')
|
||||
@app.route('/components/action_entry/<file>')
|
||||
@app.route('/components/export_popup/<file>')
|
||||
@app.route('/components/dates_popup/<file>')
|
||||
@app.route('/components/menu_popup/<file>')
|
||||
@app.route('/components/help_popup/<file>')
|
||||
@app.route('/components/help_entry/<file>')
|
||||
@app.route('/components/control/<file>')
|
||||
@app.route('/components/divider/<file>')
|
||||
@app.route('/components/states_indicator/dates/<file>')
|
||||
@app.route('/res/<file>')
|
||||
@app.route('/jsFiles/<file>')
|
||||
@app.route('/cssFiles/<file>')
|
||||
@app.route('/externalFiles/<file>')
|
||||
def subdir_file(file):
|
||||
subdir = "/".join(flask.request.path.split("/")[1:-1])
|
||||
resp = flask.send_file("client/" + subdir+"/"+file, mimetype=guess_mimetype(file))
|
||||
# resp.headers['Content-Security-Policy'] = "sandbox; script-src 'unsafe-inline';"
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/externalFiles/maps/<file>.map')
|
||||
def replace_by_empty(file):
|
||||
return ""
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def default():
|
||||
return general_file('SEAWebClient.html')
|
||||
|
||||
|
||||
@app.route('/<file>')
|
||||
def general_file(file):
|
||||
subdir = "client/"
|
||||
try:
|
||||
resp = flask.send_file(subdir+file, mimetype=guess_mimetype(file))
|
||||
except FileNotFoundError:
|
||||
logging.warning('file %s not found', file)
|
||||
return 'file not found'
|
||||
# resp.headers['Content-Security-Policy'] = "sandbox; script-src 'unsafe-inline';"
|
||||
return resp
|
||||
|
||||
|
||||
def hostport_split(hostport):
|
||||
h = hostport.split(':')
|
||||
return (h[0], int(h[1]))
|
||||
|
||||
|
||||
def handle_pdb(sig, frame):
|
||||
import pdb
|
||||
print('PDB')
|
||||
pdb.Pdb().set_trace(frame)
|
||||
|
||||
|
||||
def handle_term(sig, _):
|
||||
server.stop()
|
||||
server.close()
|
||||
|
||||
|
||||
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)
|
||||
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