264 lines
8.2 KiB
Python
264 lines
8.2 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
import tornado.ioloop
|
|
import tornado.web
|
|
import tornado.websocket
|
|
from urllib.parse import urlparse
|
|
import uuid
|
|
import datetime
|
|
import html
|
|
import argparse
|
|
import coloredlogs
|
|
import glob
|
|
import csv
|
|
|
|
|
|
|
|
|
|
class DateTimeEncoder(json.JSONEncoder):
|
|
def default(self, o):
|
|
if isinstance(o, datetime.datetime):
|
|
return o.isoformat(timespec='milliseconds')
|
|
|
|
return super().default(self, o)
|
|
|
|
class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler):
|
|
def set_extra_headers(self, path):
|
|
"""For subclass to add extra headers to the response"""
|
|
if path[-5:] == '.html':
|
|
self.set_header("Access-Control-Allow-Origin", "*")
|
|
if path[-4:] == '.svg':
|
|
self.set_header("Content-Type", "image/svg+xml")
|
|
|
|
|
|
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
|
"""
|
|
Websocket from the workers
|
|
"""
|
|
CORS_ORIGINS = ['localhost']
|
|
connections = set()
|
|
|
|
def initialize(self, config):
|
|
self.config = config
|
|
self.strokes = []
|
|
self.hasWritten = False
|
|
self.dimensions = [None, None]
|
|
|
|
def check_origin(self, origin):
|
|
parsed_origin = urlparse(origin)
|
|
# parsed_origin.netloc.lower() gives localhost:3333
|
|
valid = any([parsed_origin.hostname.endswith(origin) for origin in self.CORS_ORIGINS])
|
|
return valid
|
|
|
|
# the client connected
|
|
def open(self, p = None):
|
|
self.__class__.connections.add(self)
|
|
self.strokes = []
|
|
self.prefix = datetime.datetime.now().strftime('%Y-%m-%d-')
|
|
self.filename = self.prefix + str(self.check_filenr()) + '-' + uuid.uuid4().hex[:6]
|
|
print(self.filename)
|
|
self.write_message(json.dumps({
|
|
"filename": self.filename
|
|
}))
|
|
|
|
def check_filenr(self):
|
|
files = glob.glob(os.path.join(self.config.storage, self.prefix +'*'))
|
|
return len(files) + 1
|
|
|
|
|
|
# the client sent the message
|
|
def on_message(self, message):
|
|
logger.info(f"recieve: {message}")
|
|
|
|
try:
|
|
msg = json.loads(message)
|
|
if msg['action'] == 'stroke':
|
|
print('stroke!')
|
|
self.strokes.append([msg['color'], msg['points']])
|
|
|
|
with open(os.path.join(self.config.storage,self.filename +'.csv'), 'a') as fp:
|
|
writer = csv.writer(fp, delimiter=';')
|
|
if not self.hasWritten:
|
|
#metadata to first row, but only on demand
|
|
writer.writerow([datetime.datetime.now().strftime("%Y-%m-%d %T"), self.dimensions[0], self.dimensions[1]])
|
|
self.hasWritten = True
|
|
|
|
# first column is color, rest is points
|
|
writer.writerow([msg['color']] +[coordinate for point in msg['points'] for coordinate in point[:4]])
|
|
|
|
|
|
elif msg['action'] == 'dimensions':
|
|
self.dimensions = [int(msg['width']), int(msg['height'])]
|
|
logger.info(f"{self.dimensions=}")
|
|
|
|
|
|
else:
|
|
# self.send({'alert': 'Unknown request: {}'.format(message)})
|
|
logger.warn('Unknown request: {}'.format(message))
|
|
|
|
except Exception as e:
|
|
# self.send({'alert': 'Invalid request: {}'.format(e)})
|
|
logger.exception(e)
|
|
|
|
# client disconnected
|
|
def on_close(self):
|
|
self.__class__.rmConnection(self)
|
|
|
|
logger.info(f"Client disconnected: {self.request.remote_ip}")
|
|
|
|
|
|
@classmethod
|
|
def rmConnection(cls, client):
|
|
if client not in cls.connections:
|
|
return
|
|
cls.connections.remove(client)
|
|
|
|
@classmethod
|
|
def hasConnection(cls, client):
|
|
return client in cls.connections
|
|
|
|
|
|
class AnimationHandler(tornado.web.RequestHandler):
|
|
def initialize(self, config):
|
|
self.config = config
|
|
|
|
def get(self, filename):
|
|
self.set_header("Content-Type", "application/json")
|
|
# filename = self.get_argument("file", None)
|
|
if filename == '':
|
|
names = [f"/files/{name[:-4]}" for name in os.listdir(self.config.storage) if name not in ['.gitignore']]
|
|
self.write(json.dumps(names))
|
|
else:
|
|
path = os.path.join(self.config.storage,os.path.basename(filename)+".csv")
|
|
drawing = {
|
|
"shape": []
|
|
}
|
|
with open(path, 'r') as fp:
|
|
strokes = csv.reader(fp,delimiter=';')
|
|
for i, stroke in enumerate(strokes):
|
|
if i == 0:
|
|
# metadata on first line
|
|
drawing['time'] = stroke[0]
|
|
drawing['dimensions'] = [stroke[1], stroke[2]]
|
|
continue
|
|
color = stroke.pop(0)
|
|
points = []
|
|
for i in range(int(len(stroke) / 4)):
|
|
p = stroke[i*4:i*4+4]
|
|
points.append([float(p[0]), float(p[1]), int(p[2]), float(p[3])])
|
|
drawing['shape'].append({
|
|
'color': color,
|
|
'points': points
|
|
})
|
|
self.write(json.dumps(drawing))
|
|
|
|
def strokes2D(strokes):
|
|
# strokes to a d attribute for a path
|
|
d = "";
|
|
last_stroke = None;
|
|
cmd = "";
|
|
for stroke in strokes:
|
|
if not last_stroke:
|
|
d += f"M{stroke[0]},{stroke[1]} "
|
|
cmd = 'M'
|
|
else:
|
|
if last_stroke[2] == 1:
|
|
d += " m"
|
|
cmd = 'm'
|
|
elif cmd != 'l':
|
|
d+=' l '
|
|
cmd = 'l'
|
|
|
|
rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
|
|
d += f"{rel_stroke[0]},{rel_stroke[1]} "
|
|
last_stroke = stroke;
|
|
return d;
|
|
|
|
class Server:
|
|
"""
|
|
Server for HIT -> plotter events
|
|
As well as for the Status interface
|
|
"""
|
|
loop = None
|
|
|
|
def __init__(self, config, logger):
|
|
self.config = config
|
|
self.logger = logger
|
|
|
|
#self.config['server']['port']
|
|
self.web_root = os.path.join('www')
|
|
|
|
|
|
def start(self):
|
|
application = tornado.web.Application([
|
|
(r"/ws(.*)", WebSocketHandler, {
|
|
'config': self.config,
|
|
}),
|
|
|
|
(r"/files/(.*)", AnimationHandler,
|
|
{'config': self.config}),
|
|
(r"/(.*)", StaticFileWithHeaderHandler,
|
|
{"path": self.web_root}),
|
|
], debug=True, autoreload=True)
|
|
application.listen(self.config.port)
|
|
tornado.ioloop.IOLoop.current().start()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
argParser = argparse.ArgumentParser(
|
|
description='Start up the vector animation server')
|
|
# argParser.add_argument(
|
|
# '--config',
|
|
# '-c',
|
|
# required=True,
|
|
# type=str,
|
|
# help='The yaml config file to load'
|
|
# )
|
|
argParser.add_argument(
|
|
'--port',
|
|
type=int,
|
|
default=7890,
|
|
help='Port'
|
|
)
|
|
argParser.add_argument(
|
|
'--storage',
|
|
type=str,
|
|
default='files',
|
|
help='directory name for output files'
|
|
)
|
|
argParser.add_argument(
|
|
'--verbose',
|
|
'-v',
|
|
action='count', default=0
|
|
)
|
|
|
|
args = argParser.parse_args()
|
|
|
|
loglevel = logging.NOTSET if args.verbose > 1 else logging.DEBUG if args.verbose > 0 else logging.INFO
|
|
|
|
coloredlogs.install(
|
|
level=loglevel,
|
|
# default: "%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s"
|
|
fmt="%(asctime)s %(hostname)s %(name)s[%(process)d,%(threadName)s] %(levelname)s %(message)s"
|
|
)
|
|
|
|
# File logging
|
|
formatter = logging.Formatter(fmt='%(asctime)s %(module)s:%(lineno)d %(levelname)8s | %(message)s',
|
|
datefmt='%Y/%m/%d %H:%M:%S') # %I:%M:%S %p AM|PM format
|
|
logFileHandler = logging.handlers.RotatingFileHandler(
|
|
'log/draw_log.log',
|
|
maxBytes=1024*512,
|
|
backupCount=5
|
|
)
|
|
logFileHandler.setFormatter(formatter)
|
|
|
|
logger = logging.getLogger("sorteerhoed")
|
|
logger.addHandler(
|
|
logFileHandler
|
|
)
|
|
logger.info("Start server")
|
|
|
|
server = Server(args, logger)
|
|
server.start()
|
|
|