2019-09-11 19:00:06 +00:00
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import os
|
2019-09-11 16:16:33 +00:00
|
|
|
import tornado.ioloop
|
|
|
|
import tornado.web
|
|
|
|
import tornado.websocket
|
|
|
|
from urllib.parse import urlparse
|
2019-09-11 19:00:06 +00:00
|
|
|
import uuid
|
|
|
|
|
|
|
|
import coloredlogs
|
|
|
|
import glob
|
2019-09-11 16:16:33 +00:00
|
|
|
|
2019-10-15 10:42:59 +00:00
|
|
|
from pyaxidraw import axidraw # import module
|
|
|
|
from threading import Thread
|
|
|
|
from queue import Queue, Empty
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
2019-09-11 16:16:33 +00:00
|
|
|
|
|
|
|
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",
|
|
|
|
)
|
|
|
|
|
2019-09-11 19:00:06 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
2019-09-11 16:16:33 +00:00
|
|
|
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", "*")
|
2019-09-11 19:00:06 +00:00
|
|
|
if path[-4:] == '.svg':
|
|
|
|
self.set_header("Content-Type", "image/svg+xml")
|
2019-09-11 16:16:33 +00:00
|
|
|
|
|
|
|
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
2019-09-12 12:11:47 +00:00
|
|
|
CORS_ORIGINS = ['localhost', '.mturk.com', 'here.rubenvandeven.com']
|
2019-09-11 16:16:33 +00:00
|
|
|
connections = set()
|
2019-10-15 10:42:59 +00:00
|
|
|
|
|
|
|
def initialize(self, draw_q: Queue):
|
|
|
|
self.draw_q = draw_q
|
2019-09-11 16:16:33 +00:00
|
|
|
|
|
|
|
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)
|
2019-09-12 12:52:38 +00:00
|
|
|
logger.info(f"New client connected: {self.request.remote_ip}")
|
2019-09-11 19:00:06 +00:00
|
|
|
self.strokes = []
|
|
|
|
# self.write_message("hello!")
|
2019-09-11 16:16:33 +00:00
|
|
|
|
|
|
|
# the client sent the message
|
|
|
|
def on_message(self, message):
|
|
|
|
logger.debug(f"recieve: {message}")
|
|
|
|
try:
|
|
|
|
msg = json.loads(message)
|
2019-09-11 19:00:06 +00:00
|
|
|
# TODO: sanitize input: min/max, limit strokes
|
2019-09-11 16:16:33 +00:00
|
|
|
if msg['action'] == 'move':
|
2019-09-11 19:00:06 +00:00
|
|
|
# TODO: min/max input
|
2019-10-15 10:42:59 +00:00
|
|
|
point = [float(msg['direction'][0]),float(msg['direction'][1]), bool(msg['mouse'])]
|
2019-09-11 19:00:06 +00:00
|
|
|
self.strokes.append(point)
|
2019-10-15 10:42:59 +00:00
|
|
|
self.draw_q.put(point)
|
2019-09-11 19:00:06 +00:00
|
|
|
|
2019-09-11 16:16:33 +00:00
|
|
|
elif msg['action'] == 'up':
|
|
|
|
logger.info(f'up: {msg}')
|
2019-09-11 19:00:06 +00:00
|
|
|
point = [msg['direction'][0],msg['direction'][1], 1]
|
|
|
|
self.strokes.append(point)
|
|
|
|
|
2019-09-11 16:16:33 +00:00
|
|
|
elif msg['action'] == 'submit':
|
|
|
|
logger.info(f'up: {msg}')
|
2019-09-11 19:00:06 +00:00
|
|
|
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}"
|
|
|
|
}))
|
2019-09-11 16:16:33 +00:00
|
|
|
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)
|
2019-09-12 12:52:38 +00:00
|
|
|
logger.info(f"Client disconnected: {self.request.remote_ip}")
|
2019-09-11 19:00:06 +00:00
|
|
|
|
|
|
|
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
|
2019-09-11 16:16:33 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def rmConnection(cls, client):
|
|
|
|
if client not in cls.connections:
|
|
|
|
return
|
|
|
|
cls.connections.remove(client)
|
2019-09-11 19:00:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
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())
|
2019-09-11 16:16:33 +00:00
|
|
|
|
2019-10-15 10:42:59 +00:00
|
|
|
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()
|
|
|
|
|
2019-09-11 16:16:33 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
args = argParser.parse_args()
|
2019-09-11 19:00:06 +00:00
|
|
|
|
2019-09-11 16:16:33 +00:00
|
|
|
coloredlogs.install(
|
|
|
|
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
|
|
)
|
2019-10-15 10:42:59 +00:00
|
|
|
|
2019-09-12 12:59:03 +00:00
|
|
|
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)
|
2019-09-11 16:16:33 +00:00
|
|
|
logger.addHandler(
|
2019-09-12 12:59:03 +00:00
|
|
|
logFileHandler
|
2019-09-11 16:16:33 +00:00
|
|
|
)
|
2019-10-15 10:42:59 +00:00
|
|
|
|
|
|
|
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()
|