guest_worker/server.py
2019-10-15 12:42:59 +02:00

247 lines
7.9 KiB
Python

import argparse
import json
import logging
import os
import tornado.ioloop
import tornado.web
import tornado.websocket
from urllib.parse import urlparse
import uuid
import coloredlogs
import glob
from pyaxidraw import axidraw # import module
from threading import Thread
from queue import Queue, Empty
import threading
logger = logging.getLogger("drawing")
argParser = argparse.ArgumentParser(description='Start up the server to have non-Mechanical Turks draw their sketches.')
argParser.add_argument(
'--port',
'-p',
default=8888,
help='The port for the server to listen'
)
argParser.add_argument(
'--verbose',
'-v',
action="store_true",
)
generated_image_dir = os.path.join('www','generated')
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 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):
CORS_ORIGINS = ['localhost', '.mturk.com', 'here.rubenvandeven.com']
connections = set()
def initialize(self, draw_q: Queue):
self.draw_q = draw_q
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)
logger.info(f"New client connected: {self.request.remote_ip}")
self.strokes = []
# self.write_message("hello!")
# the client sent the message
def on_message(self, message):
logger.debug(f"recieve: {message}")
try:
msg = json.loads(message)
# TODO: sanitize input: min/max, limit strokes
if msg['action'] == 'move':
# TODO: min/max input
point = [float(msg['direction'][0]),float(msg['direction'][1]), bool(msg['mouse'])]
self.strokes.append(point)
self.draw_q.put(point)
elif msg['action'] == 'up':
logger.info(f'up: {msg}')
point = [msg['direction'][0],msg['direction'][1], 1]
self.strokes.append(point)
elif msg['action'] == 'submit':
logger.info(f'up: {msg}')
id = self.submit_strokes()
if not id:
self.write_message(json.dumps('error'))
return
self.write_message(json.dumps({
'action': 'submitted',
'msg': f"Submission ok, please refer to your submission as: {id}"
}))
elif msg['action'] == 'down':
# not used, implicit in move?
pass
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}")
def submit_strokes(self):
if len(self.strokes) < 1:
return False
d = strokes2D(self.strokes)
svg = f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 600 600"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
>
<path d="{d}" style="stroke:black;stroke-width:2;fill:none;" />
</svg>
"""
id = uuid.uuid4().hex
filename = os.path.join(generated_image_dir , id+'.svg')
with open(filename, 'w') as fp:
logger.info(f"Wrote {filename}")
fp.write(svg)
return id
@classmethod
def rmConnection(cls, client):
if client not in cls.connections:
return
cls.connections.remove(client)
class LatestImageHandler(tornado.web.RequestHandler):
def get(self):
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
self.set_header("Content-Type", "image/svg+xml")
list_of_files = glob.glob(os.path.join(generated_image_dir,'*.svg'))
latest_file = max(list_of_files, key=os.path.getctime)
with open(latest_file, 'r') as fp:
self.write(fp.read())
def axiDrawCueListener(q: Queue, isRunning: threading.Event):
ad = axidraw.AxiDraw()
try:
ad.interactive()
ad.connect()
ad.options.units = 1 # set to use centimeters instead of inches
ad.options.accel = 100;
ad.options.speed_penup = 100
ad.options.speed_pendown = 100
ad.options.model = 2 # A3, set to 1 for A4
ad.moveto(0,0)
plotterWidth = 22
plotterHeight = 18 # 16?
while isRunning.is_set():
# TODO: set timeout on .get() with catch block, so we can escape if no moves come in
try:
move = draw_q.get(True, 1)
except Empty as e:
logger.log(5, "Empty queue.")
else:
ad.moveto(move[0]* plotterWidth, move[1]*plotterHeight)
print('handler!',move)
except Exception as e:
logger.exception(e)
finally:
logger.warning("Close Axidraw connection")
ad.moveto(0,0)
ad.disconnect()
if __name__ == "__main__":
args = argParser.parse_args()
coloredlogs.install(
level=logging.DEBUG if args.verbose else logging.INFO,
)
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(
'mt_server.log',
maxBytes=1024*512,
backupCount=5
)
logFileHandler.setFormatter(formatter)
logger.addHandler(
logFileHandler
)
draw_q = Queue()
isRunning = threading.Event()
isRunning.set()
thread = Thread(target = axiDrawCueListener, args = (draw_q, isRunning))
thread.start()
try:
application = tornado.web.Application([
(r"/ws(.*)", WebSocketHandler, {'draw_q': draw_q}),
(r"/latest.svg", LatestImageHandler), # TODO: have js request the right image, based on a 'start' button. This way we can trace the history of a drawing
(r"/(.*)", StaticFileWithHeaderHandler,
{"path": 'www', "default_filename": 'index.html'}),
], debug=True)
application.listen(args.port)
tornado.ioloop.IOLoop.current().start()
finally:
isRunning.clear()