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
2019-10-23 08:56:28 +00:00
from threading import Thread , Event
2019-10-15 10:42:59 +00:00
from queue import Queue , Empty
import threading
2019-10-23 08:56:28 +00:00
from server_test import generated_image_dir
import asyncio
2019-10-15 10:42:59 +00:00
2019-10-23 08:56:28 +00:00
logger = logging . getLogger ( " sorteerhoed " ) . getChild ( " webserver " )
2019-09-11 19:00:06 +00:00
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
2019-10-23 08:56:28 +00:00
def initialize ( self , draw_q : Queue , generated_image_dir : str ) :
2019-10-15 10:42:59 +00:00
self . draw_q = draw_q
2019-10-23 08:56:28 +00:00
self . generated_image_dir = generated_image_dir
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
2019-10-23 08:56:28 +00:00
filename = os . path . join ( self . generated_image_dir , id + ' .svg ' )
2019-09-11 19:00:06 +00:00
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 ) :
2019-10-23 08:56:28 +00:00
def initialize ( self , generated_image_dir : str ) :
self . generated_image_dir = generated_image_dir
2019-09-11 19:00:06 +00:00
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 " )
2019-10-23 08:56:28 +00:00
list_of_files = glob . glob ( os . path . join ( self . generated_image_dir , ' *.svg ' ) )
2019-09-11 19:00:06 +00:00
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-23 08:56:28 +00:00
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
2019-10-15 10:42:59 +00:00
2019-10-23 08:56:28 +00:00
TODO : change to have the HIT_id as param to the page . Load hit from storage with previous image
"""
def __init__ ( self , config , eventQ : Queue , runningEvent : Event , plotterQ : Queue ) :
self . isRunning = runningEvent
self . eventQ = eventQ
self . config = config
self . logger = logger
2019-10-15 10:42:59 +00:00
2019-10-23 08:56:28 +00:00
self . plotterQ = plotterQ # communicate directly to plotter (skip main thread)
2019-10-15 10:42:59 +00:00
2019-10-23 08:56:28 +00:00
#self.config['server']['port']
self . generated_image_dir = os . path . join ( ' www ' , ' generated ' )
self . static_file_dir = os . path . join ( ' www ' )
2019-10-15 10:42:59 +00:00
2019-10-23 08:56:28 +00:00
self . server_loop = None
def start ( self ) :
try :
asyncio . set_event_loop ( asyncio . new_event_loop ( ) )
application = tornado . web . Application ( [
( r " /ws(.*) " , WebSocketHandler , { ' draw_q ' : self . plotterQ , ' generated_image_dir ' : self . generated_image_dir } ) ,
( r " /latest.svg " , LatestImageHandler , { ' generated_image_dir ' : self . generated_image_dir } ) , # 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 " : self . static_file_dir , " default_filename " : ' index.html ' } ) ,
] , debug = True , autoreload = False )
application . listen ( self . config [ ' server ' ] [ ' port ' ] )
self . server_loop = tornado . ioloop . IOLoop . current ( )
if self . isRunning . is_set ( ) :
self . server_loop . start ( )
finally :
self . logger . info ( " Stopping webserver " )
self . isRunning . clear ( )
def stop ( self ) :
if self . server_loop :
self . logger . debug ( " Got call to stop " )
self . server_loop . asyncio_loop . call_soon_threadsafe ( self . _stop )
2019-10-15 10:42:59 +00:00
2019-10-23 08:56:28 +00:00
def _stop ( self ) :
self . server_loop . stop ( )