Send svg image to MTer, park pen on start, no-plotter option, max task time dynamic, fix mt_task.xml click

This commit is contained in:
Ruben van de Ven 2019-11-02 20:45:57 +01:00
parent a2c20b533c
commit 3111d2cc85
7 changed files with 69 additions and 17 deletions

View file

@ -38,7 +38,7 @@
<tbody> <tbody>
<tr> <tr>
<td><label>Drawing link:</label></td> <td><label>Drawing link:</label></td>
<td><a href="http://here.rubenvandeven.com:8888/draw?id={HIT_NR}">drawing page</a></td> <td><a target="_blank" href="http://here.rubenvandeven.com:8888/draw?id={HIT_NR}">drawing page</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -16,6 +16,11 @@ if __name__ == '__main__':
type=str, type=str,
help='The yaml config file to load' help='The yaml config file to load'
) )
argParser.add_argument(
'--no-plotter',
action='store_true',
help='Skip attempt to connect to plotter'
)
argParser.add_argument( argParser.add_argument(
'--verbose', '--verbose',
'-v', '-v',
@ -49,5 +54,5 @@ if __name__ == '__main__':
logger.info("Start server") logger.info("Start server")
command = CentralManagement(debug_mode=args.verbose > 0) command = CentralManagement(debug_mode=args.verbose > 0)
command.loadConfig(args.config) command.loadConfig(args.config, args)
command.start() command.start()

View file

@ -67,6 +67,9 @@ class HIT(Base):
def getSvgImageUrl(self): def getSvgImageUrl(self):
return f"scans/{self.id:06d}.svg" return f"scans/{self.id:06d}.svg"
def getSvgImagePath(self):
return os.path.join('www', self.getSvgImageUrl())
def getStatus(self): def getStatus(self):
if self.scanned_at: if self.scanned_at:
return "completed" return "completed"
@ -155,6 +158,9 @@ class Store:
return int(2.5*60) return int(2.5*60)
return int(sum(durations) / len(durations)) return int(sum(durations) / len(durations))
def getEstimatedHitDuration(self):
return self.getAvgDurationOfPreviousNHits(5)
def getHITs(self, n = 100): def getHITs(self, n = 100):
return self.session.query(HIT).\ return self.session.query(HIT).\
filter(HIT.submit_hit_at != None).\ filter(HIT.submit_hit_at != None).\

View file

@ -40,11 +40,14 @@ class CentralManagement():
self.scanLock = threading.Lock() self.scanLock = threading.Lock()
def loadConfig(self, filename): def loadConfig(self, filename, args):
with open(filename, 'r') as fp: with open(filename, 'r') as fp:
self.logger.debug('Load config from {}'.format(filename)) self.logger.debug('Load config from {}'.format(filename))
self.config = yaml.safe_load(fp) self.config = yaml.safe_load(fp)
if args.no_plotter:
self.config['dummy_plotter'] = True
varDb = os.path.join( varDb = os.path.join(
# self.config['storage_dir'], # self.config['storage_dir'],
'hit_store.db' 'hit_store.db'
@ -242,7 +245,7 @@ class CentralManagement():
self.logger.info(f"Make HIT {self.currentHit.id}") self.logger.info(f"Make HIT {self.currentHit.id}")
question = open(self.config['amazon']['task_xml'], mode='r').read().replace("{HIT_NR}",str(self.currentHit.id)) question = open(self.config['amazon']['task_xml'], mode='r').read().replace("{HIT_NR}",str(self.currentHit.id))
estimatedHitDuration = self.store.getAvgDurationOfPreviousNHits(5) estimatedHitDuration = self.store.getEstimatedHitDuration()
fee = (self.config['hour_rate_aim']/3600.) * estimatedHitDuration fee = (self.config['hour_rate_aim']/3600.) * estimatedHitDuration
self.currentHit.fee = fee self.currentHit.fee = fee
@ -254,7 +257,7 @@ class CentralManagement():
Reward = "{:.2f}".format(fee), Reward = "{:.2f}".format(fee),
MaxAssignments = 1, MaxAssignments = 1,
LifetimeInSeconds = self.config['hit_lifetime'], LifetimeInSeconds = self.config['hit_lifetime'],
AssignmentDurationInSeconds = self.config['hit_assignment_duration'], AssignmentDurationInSeconds = estimatedHitDuration * 2, # give people twice as long as we expect them to take
AutoApprovalDelayInSeconds = self.config['hit_autoapprove_delay'], AutoApprovalDelayInSeconds = self.config['hit_autoapprove_delay'],
Question = question, Question = question,
) )
@ -317,6 +320,7 @@ class CentralManagement():
def reset(self) -> str: def reset(self) -> str:
# TODO: for returns & abandons # TODO: for returns & abandons
self.plotter.park()
scan = threading.Thread(target=self.cleanDrawing, name='reset') scan = threading.Thread(target=self.cleanDrawing, name='reset')
scan.start() scan.start()

View file

@ -53,10 +53,9 @@ class Plotter:
self.ad.options.speed_pendown = 100 self.ad.options.speed_pendown = 100
self.ad.options.model = 1 # 2 for A3, 1 for A4 self.ad.options.model = 1 # 2 for A3, 1 for A4
self.ad.moveto(0,0)
# plotterWidth = 25 self.park()
# plotterHeight = 21 # self.ad.moveto(0,0)
else: else:
self.ad = None self.ad = None
@ -66,11 +65,15 @@ class Plotter:
finally: finally:
self.logger.warning("Close Axidraw connection") self.logger.warning("Close Axidraw connection")
if self.ad: if self.ad:
with self.scannerLock: try:
self.ad.moveto(0,0) with self.scannerLock:
self.ad.disconnect() self.ad.moveto(0,0)
self.ad.disconnect()
except Exception as e:
self.logger.warning("Error on closing axidraw:")
self.logger.exception(e)
self.logger.critical("Clear running Event") self.logger.info("Clear running Event")
# send shutdown signal (if not already set) # send shutdown signal (if not already set)
self.isRunning.clear() self.isRunning.clear()

View file

@ -16,6 +16,7 @@ import httpagentparser
import geoip2.database import geoip2.database
import queue import queue
import datetime import datetime
import html
logger = logging.getLogger("sorteerhoed").getChild("webserver") logger = logging.getLogger("sorteerhoed").getChild("webserver")
@ -62,6 +63,8 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.hit = self.store.currentHit self.hit = self.store.currentHit
self.timeout = datetime.datetime.now() + datetime.timedelta(seconds=self.store.getEstimatedHitDuration() * 2)
if self.hit.submit_hit_at: if self.hit.submit_hit_at:
raise Exception("Opening websocket for already submitted hit") raise Exception("Opening websocket for already submitted hit")
@ -80,6 +83,12 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
# the client sent the message # the client sent the message
def on_message(self, message): def on_message(self, message):
logger.debug(f"recieve: {message}") logger.debug(f"recieve: {message}")
if datetime.datetime.now() > self.timeout:
logger.critical("Close websocket after timeout (abandon?)")
self.close()
return
try: try:
msg = json.loads(message) msg = json.loads(message)
# TODO: sanitize input: min/max, limit strokes # TODO: sanitize input: min/max, limit strokes
@ -100,6 +109,19 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
if not id: if not id:
self.write_message(json.dumps('error')) self.write_message(json.dumps('error'))
return return
#store svg:
d = html.escape(msg['d'])
svg = f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.0" viewBox="0 0 {self.config['scanner']['width']}0 {self.config['scanner']['height']}0" width="{self.config['scanner']['width']}mm" height="{self.config['scanner']['height']}mm" preserveAspectRatio="none">
<path d="{d}" style='stroke:gray;stroke-width:1mm;fill:none;' id="stroke" />
</svg>
"""
with open(self.store.currentHit.getSvgImagePath(), 'w') as fp:
fp.write(svg)
self.write_message(json.dumps({ self.write_message(json.dumps({
'action': 'submitted', 'action': 'submitted',

View file

@ -13,6 +13,17 @@
width:100%; width:100%;
height:100%; height:100%;
font-family: sans-serif; font-family: sans-serif;
z-index:2;
}
img{
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
width:100%;
height:100%;
z-index:1;
} }
path { path {
@ -81,11 +92,10 @@
background:gray; background:gray;
} }
#interface{ #interface{
background:white; background:black;
height: 0; height: 0;
overflow: hidden; overflow: hidden;
padding-top: calc({HEIGHT}/{WIDTH} * 100%); padding-top: calc({HEIGHT}/{WIDTH} * 100%);
background: white;
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
background-size: 100% 100%; background-size: 100% 100%;
@ -117,17 +127,18 @@
</style> </style>
</head> </head>
<body> <body>
<div id='interface' style="background-image:url('{IMAGE_URL}')"> <div id='interface'">
<div id='innerface'> <div id='innerface'>
<div id='wrapper'> <div id='wrapper'>
<!-- <img src="{IMAGE_URL}" id='sample'>--> <!-- <img src="{IMAGE_URL}" id='sample'>-->
<svg id="canvas" viewBox="0 0 {WIDTH}0 {HEIGHT}0" width="{WIDTH}mm" height="{HEIGHT}mm" preserveAspectRatio="none"> <svg id="canvas" viewBox="0 0 {WIDTH}0 {HEIGHT}0" width="{WIDTH}mm" height="{HEIGHT}mm" preserveAspectRatio="none">
<path d="" id="stroke" /> <path d="" id="stroke" />
</svg> </svg>
<img src='{IMAGE_URL}'>
</div> </div>
<div id='info'> <div id='info'>
<ul> <ul>
<li>Drag the mouse to trace the <strong>clearest lines</strong> drawing above</li> <li>Drag the mouse to trace the lines drawing above</li>
<li>Follow the lines as precise as possible</li> <li>Follow the lines as precise as possible</li>
<li>Press submit when you're done.</li> <li>Press submit when you're done.</li>
<li>You'll receive a submission token, to fill in at Mechanical Turk</li> <li>You'll receive a submission token, to fill in at Mechanical Turk</li>
@ -304,7 +315,8 @@
} }
socket.send(JSON.stringify({ socket.send(JSON.stringify({
'action': 'submit' 'action': 'submit',
'd': strokeEl.getAttribute('d')
})); }));
document.body.removeEventListener('mousemove', draw); document.body.removeEventListener('mousemove', draw);