2019-10-23 10:56:28 +02:00
|
|
|
import logging
|
|
|
|
import yaml
|
|
|
|
from sorteerhoed import HITStore
|
|
|
|
import os
|
|
|
|
import subprocess
|
|
|
|
import boto3
|
|
|
|
import threading
|
|
|
|
from queue import Queue
|
|
|
|
from sorteerhoed.plotter import Plotter
|
|
|
|
import queue
|
|
|
|
from sorteerhoed.sqs import SqsListener
|
|
|
|
from sorteerhoed.webserver import Server
|
|
|
|
import time
|
2019-10-23 22:33:37 +02:00
|
|
|
from sorteerhoed.Signal import Signal
|
|
|
|
import io
|
|
|
|
from PIL import Image
|
|
|
|
import datetime
|
2019-11-01 18:53:42 +01:00
|
|
|
from shutil import copyfile
|
2020-01-08 17:55:45 +01:00
|
|
|
import colorsys
|
2020-01-13 13:49:59 +01:00
|
|
|
import tqdm
|
2019-10-23 10:56:28 +02:00
|
|
|
|
|
|
|
|
2020-01-08 17:55:45 +01:00
|
|
|
class Level(object):
|
2020-01-13 13:49:59 +01:00
|
|
|
"""
|
|
|
|
Level image effect adapted from https://stackoverflow.com/a/3125421
|
|
|
|
"""
|
|
|
|
|
2020-01-08 17:55:45 +01:00
|
|
|
def __init__(self, minv, maxv, gamma):
|
|
|
|
self.minv= minv/255.0
|
|
|
|
self.maxv= maxv/255.0
|
|
|
|
self._interval= self.maxv - self.minv
|
|
|
|
self._invgamma= 1.0/gamma
|
|
|
|
|
|
|
|
def new_level(self, value):
|
|
|
|
if value <= self.minv: return 0.0
|
|
|
|
if value >= self.maxv: return 1.0
|
|
|
|
return ((value - self.minv)/self._interval)**self._invgamma
|
|
|
|
|
|
|
|
def convert_and_level(self, band_values):
|
|
|
|
h, s, v= colorsys.rgb_to_hsv(*(i/255.0 for i in band_values))
|
|
|
|
new_v= self.new_level(v)
|
|
|
|
return tuple(int(255*i)
|
|
|
|
for i
|
|
|
|
in colorsys.hsv_to_rgb(h, s, new_v))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def level_image(cls, image, minv=0, maxv=255, gamma=1.0):
|
|
|
|
"""Level the brightness of image (a PIL.Image instance)
|
|
|
|
All values ≤ minv will become 0
|
|
|
|
All values ≥ maxv will become 255
|
|
|
|
gamma controls the curve for all values between minv and maxv"""
|
|
|
|
|
|
|
|
if image.mode != "RGB":
|
|
|
|
raise ValueError("this works with RGB images only")
|
|
|
|
|
|
|
|
new_image= image.copy()
|
|
|
|
|
|
|
|
leveller= cls(minv, maxv, gamma)
|
|
|
|
levelled_data= [
|
|
|
|
leveller.convert_and_level(data)
|
|
|
|
for data in image.getdata()]
|
|
|
|
new_image.putdata(levelled_data)
|
|
|
|
return new_image
|
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
class CentralManagement():
|
|
|
|
"""
|
|
|
|
Central management reads config and controls process flow
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
The HIT Store is the archive of hits
|
|
|
|
mturk thread communicates with mturk
|
|
|
|
server thread is tornado communicating with the turkers and with the status interface on the installation
|
|
|
|
Plotter thread reads plotter queue and sends it to there
|
|
|
|
Scanner is for now a simpe imagescan command
|
|
|
|
SQS: thread that listens for updates from Amazon
|
|
|
|
"""
|
|
|
|
def __init__(self, debug_mode):
|
|
|
|
self.logger = logging.getLogger("sorteerhoed").getChild('management')
|
|
|
|
self.debug = debug_mode
|
|
|
|
self.currentHit = None
|
2019-11-05 10:47:23 +01:00
|
|
|
self.lastHitTime = None
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
self.eventQueue = Queue()
|
2020-01-10 18:03:18 +01:00
|
|
|
self.statusPageQueue = Queue()
|
2019-10-23 10:56:28 +02:00
|
|
|
self.isRunning = threading.Event()
|
2019-10-30 12:44:25 +01:00
|
|
|
self.isScanning = threading.Event()
|
2019-11-01 17:18:28 +01:00
|
|
|
self.scanLock = threading.Lock()
|
2019-11-04 09:24:37 +01:00
|
|
|
self.notPaused = threading.Event()
|
2020-01-13 13:49:59 +01:00
|
|
|
self.lightStatus = 0
|
2019-10-23 10:56:28 +02:00
|
|
|
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 20:45:57 +01:00
|
|
|
def loadConfig(self, filename, args):
|
2019-11-04 09:24:37 +01:00
|
|
|
self.configFile = filename
|
|
|
|
self.args = args
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-04 09:24:37 +01:00
|
|
|
self.reloadConfig()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
varDb = os.path.join(
|
|
|
|
# self.config['storage_dir'],
|
|
|
|
'hit_store.db'
|
2020-01-08 17:55:45 +01:00
|
|
|
)
|
2019-10-23 10:56:28 +02:00
|
|
|
self.store = HITStore.Store(varDb, logLevel=logging.DEBUG if self.debug else logging.INFO)
|
2020-01-13 13:49:59 +01:00
|
|
|
self.store.registerUpdateHook(self.updateLightHook) # change light based on status
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
self.logger.debug(f"Loaded configuration: {self.config}")
|
|
|
|
|
2019-11-04 09:24:37 +01:00
|
|
|
def reloadConfig(self):
|
|
|
|
# reload config settings
|
|
|
|
with open(self.configFile, 'r') as fp:
|
|
|
|
self.logger.debug('Load config from {}'.format(self.configFile))
|
|
|
|
self.config = yaml.safe_load(fp)
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-04 09:24:37 +01:00
|
|
|
if self.args.no_plotter:
|
|
|
|
self.config['dummy_plotter'] = True
|
|
|
|
self.config['for_real'] = self.args.for_real
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
|
|
|
|
# self.panopticon = Panopticon(self, self.config, self.voiceStorage)
|
|
|
|
def start(self):
|
|
|
|
self.isRunning.set()
|
2019-11-04 09:24:37 +01:00
|
|
|
self.notPaused.set()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
try:
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
# M-turk connection
|
|
|
|
MTURK_SANDBOX = 'https://mturk-requester-sandbox.us-east-1.amazonaws.com'
|
2019-11-02 21:23:38 +01:00
|
|
|
MTURK_REAL = 'https://mturk-requester.us-east-1.amazonaws.com'
|
2019-10-23 10:56:28 +02:00
|
|
|
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/mturk.html#MTurk.Client
|
|
|
|
self.mturk = boto3.client('mturk',
|
|
|
|
aws_access_key_id = self.config['amazon']['user_id'],
|
|
|
|
aws_secret_access_key = self.config['amazon']['user_secret'],
|
|
|
|
region_name='us-east-1',
|
2019-11-02 21:23:38 +01:00
|
|
|
endpoint_url = MTURK_REAL if self.config['for_real'] else MTURK_SANDBOX
|
2019-10-23 10:56:28 +02:00
|
|
|
)
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 21:23:38 +01:00
|
|
|
self.logger.info(f"Mechanical turk account balance: {self.mturk.get_account_balance()['AvailableBalance']}")
|
2019-12-18 18:49:07 +01:00
|
|
|
if not self.config['for_real']:
|
|
|
|
self.logger.info("Remove block from sandbox worker account")
|
|
|
|
self.mturk.delete_worker_block(WorkerId='A1CK46PK9VEUH5', Reason='Myself on Sandbox')
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
# clear any pending hits:
|
|
|
|
pending_hits = self.mturk.list_hits(MaxResults=100)
|
|
|
|
for pending_hit in pending_hits['HITs']:
|
|
|
|
# print(pending_hit['HITId'], pending_hit['HITStatus'])
|
|
|
|
if pending_hit['HITStatus'] == 'Assignable':
|
|
|
|
self.logger.warn(f"Expire stale hit: {pending_hit['HITId']}: {pending_hit['HITStatus']}")
|
|
|
|
self.mturk.update_expiration_for_hit(
|
|
|
|
HITId=pending_hit['HITId'],
|
|
|
|
ExpireAt=datetime.datetime.fromisoformat('2015-01-01')
|
|
|
|
)
|
|
|
|
self.mturk.delete_hit(HITId=pending_hit['HITId'])
|
2020-01-13 13:49:59 +01:00
|
|
|
staleHit = self.store.getHitByRemoteId(pending_hit['HITId'])
|
|
|
|
staleHit.delete()
|
|
|
|
self.store.saveHIT(staleHit)
|
|
|
|
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
self.sqs = SqsListener(self.config, self.eventQueue, self.isRunning)
|
2019-10-23 22:33:37 +02:00
|
|
|
sqsThread = threading.Thread(target=self.sqs.start, name='sqs')
|
2019-10-23 10:56:28 +02:00
|
|
|
sqsThread.start()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
# the plotter itself
|
2019-11-01 17:18:28 +01:00
|
|
|
self.plotter = Plotter(self.config, self.eventQueue, self.isRunning, self.scanLock)
|
2019-10-23 22:33:37 +02:00
|
|
|
plotterThread = threading.Thread(target=self.plotter.start, name='plotter')
|
2019-10-23 10:56:28 +02:00
|
|
|
plotterThread.start()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
# webserver for turks and status
|
2019-10-23 22:33:37 +02:00
|
|
|
self.server = Server(self.config, self.eventQueue, self.isRunning, self.plotter.q, self.store)
|
|
|
|
serverThread = threading.Thread(target=self.server.start, name='server')
|
2019-10-23 10:56:28 +02:00
|
|
|
serverThread.start()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
# event listener:
|
2019-10-23 22:33:37 +02:00
|
|
|
dispatcherThread = threading.Thread(target=self.eventListener, name='dispatcher')
|
2019-10-23 10:56:28 +02:00
|
|
|
dispatcherThread.start()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 21:45:55 +01:00
|
|
|
self.eventQueue.put(Signal('start', {'ding':'test'}))
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 22:33:37 +02:00
|
|
|
while self.isRunning.is_set():
|
|
|
|
time.sleep(.5)
|
2019-11-02 22:31:16 +01:00
|
|
|
except Exception as e:
|
|
|
|
self.logger.exception(e)
|
2019-10-23 10:56:28 +02:00
|
|
|
finally:
|
2019-11-01 18:53:42 +01:00
|
|
|
self.logger.warning("Stopping Central Managment")
|
2019-10-23 10:56:28 +02:00
|
|
|
self.isRunning.clear()
|
|
|
|
self.server.stop()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
self.expireCurrentHit()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
def expireCurrentHit(self):
|
2020-01-13 13:49:59 +01:00
|
|
|
if self.currentHit and not self.currentHit.isConfirmed():
|
|
|
|
if self.currentHit.hit_id: # hit pending at Amazon
|
|
|
|
self.logger.warn(f"Expire hit: {self.currentHit.hit_id}")
|
2019-11-02 22:31:16 +01:00
|
|
|
self.mturk.update_expiration_for_hit(
|
|
|
|
HITId=self.currentHit.hit_id,
|
|
|
|
ExpireAt=datetime.datetime.fromisoformat('2015-01-01')
|
|
|
|
)
|
2019-11-03 16:26:08 +01:00
|
|
|
try:
|
|
|
|
self.mturk.delete_hit(HITId=self.currentHit.hit_id)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.exception(e)
|
2020-01-13 13:49:59 +01:00
|
|
|
|
|
|
|
if not self.currentHit.isSubmitted():
|
|
|
|
self.currentHit.delete()
|
|
|
|
self.store.saveHIT(self.currentHit)
|
|
|
|
|
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
def eventListener(self):
|
|
|
|
while self.isRunning.is_set():
|
|
|
|
try:
|
|
|
|
signal = self.eventQueue.get(True, 1)
|
|
|
|
except queue.Empty:
|
|
|
|
pass
|
|
|
|
# self.logger.log(5, "Empty queue.")
|
|
|
|
else:
|
2019-11-02 22:31:16 +01:00
|
|
|
try:
|
|
|
|
"""
|
|
|
|
Possible events:
|
|
|
|
- SQS events: accept/abandoned/returned/submitted
|
|
|
|
- webserver events: open, draw, submit
|
|
|
|
- scan complete
|
|
|
|
- HIT created
|
|
|
|
- Plotter complete
|
2020-01-08 17:55:45 +01:00
|
|
|
-
|
2019-11-02 22:31:16 +01:00
|
|
|
"""
|
|
|
|
self.logger.info(f"SIGNAL: {signal}")
|
|
|
|
if signal.name == 'start':
|
|
|
|
self.makeHit()
|
2019-11-05 10:47:23 +01:00
|
|
|
self.lastHitTime = datetime.datetime.now()
|
2020-01-10 18:03:18 +01:00
|
|
|
elif signal.name == 'hit.scan':
|
2020-01-13 13:49:59 +01:00
|
|
|
# start a scan
|
2020-01-10 18:03:18 +01:00
|
|
|
if signal.params['id'] != self.currentHit.id:
|
2020-01-13 13:49:59 +01:00
|
|
|
self.logger.info(f"Hit.scan had wrong id: {signal}")
|
2020-01-10 18:03:18 +01:00
|
|
|
continue
|
2020-01-13 13:49:59 +01:00
|
|
|
# self.statusPageQueue.add(dict(hit_id=signal.params['id'], transition='scanning'))
|
2020-01-10 18:03:18 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
elif signal.name == 'hit.scanned':
|
|
|
|
# TODO: wrap up hit & make new HIT
|
2020-01-13 13:49:59 +01:00
|
|
|
if signal.params['hit_id'] != self.currentHit.id:
|
2020-01-10 18:03:18 +01:00
|
|
|
self.logger.info(f"Hit.scanned had wrong id: {signal}")
|
|
|
|
continue
|
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
self.currentHit.scanned_at = datetime.datetime.utcnow()
|
2020-01-13 16:13:42 +01:00
|
|
|
self.store.saveHIT(self.currentHit)
|
|
|
|
|
2019-11-05 10:47:23 +01:00
|
|
|
time_diff = datetime.datetime.now() - self.lastHitTime
|
2019-12-18 18:49:07 +01:00
|
|
|
to_wait = 10 - time_diff.total_seconds()
|
2020-01-13 13:49:59 +01:00
|
|
|
# self.statusPageQueue.add(dict(hit_id=self.currentHit.id, state='scan'))
|
2020-01-10 18:03:18 +01:00
|
|
|
|
2019-11-05 10:47:23 +01:00
|
|
|
if to_wait > 0:
|
|
|
|
self.logger.warn(f"Sleep until next hit: {to_wait}s")
|
|
|
|
time.sleep(to_wait)
|
|
|
|
else:
|
|
|
|
self.logger.info(f"No need to wait: {to_wait}s")
|
2020-01-10 18:03:18 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
self.makeHit()
|
2019-11-05 10:47:23 +01:00
|
|
|
self.lastHitTime = datetime.datetime.now()
|
2020-01-10 18:03:18 +01:00
|
|
|
elif signal.name == 'hit.creating':
|
2020-01-13 13:49:59 +01:00
|
|
|
# self.statusPageQueue.add(dict(hit_id=signal.params['id'], transition='create_hit'))
|
|
|
|
pass
|
2020-01-10 18:03:18 +01:00
|
|
|
elif signal.name == 'hit.created':
|
2020-01-13 13:49:59 +01:00
|
|
|
# self.statusPageQueue.add(dict(hit_id=signal.params['id'], remote_id=signal.params['remote_id'], state='hit'))
|
|
|
|
pass
|
2019-11-02 22:31:16 +01:00
|
|
|
elif signal.name == 'scan.start':
|
|
|
|
pass
|
|
|
|
elif signal.name == 'scan.finished':
|
2020-01-10 18:03:18 +01:00
|
|
|
# probably see hit.scanned
|
2019-11-02 22:31:16 +01:00
|
|
|
pass
|
2020-01-10 18:03:18 +01:00
|
|
|
|
|
|
|
elif signal.name == 'hit.assignment':
|
|
|
|
# Create new assignment
|
2019-11-02 22:31:16 +01:00
|
|
|
if signal.params['hit_id'] != self.currentHit.id:
|
2019-10-30 12:44:25 +01:00
|
|
|
continue
|
2020-01-13 13:49:59 +01:00
|
|
|
assignment = self.currentHit.getAssignmentById(signal.params['assignment_id'])
|
|
|
|
|
2020-01-10 18:03:18 +01:00
|
|
|
elif signal.name == 'assignment.info':
|
|
|
|
assignment = self.currentHit.getAssignmentById(signal.params['assignment_id'])
|
|
|
|
if not assignment:
|
|
|
|
self.logger.warning(f"assignment.info assignment.id not for current hit assignments: {signal}")
|
|
|
|
|
2020-01-13 13:49:59 +01:00
|
|
|
change = False
|
2019-11-02 22:31:16 +01:00
|
|
|
for name, value in signal.params.items():
|
|
|
|
if name == 'ip':
|
2020-01-10 18:03:18 +01:00
|
|
|
assignment.turk_ip = value
|
2019-11-02 22:31:16 +01:00
|
|
|
if name == 'location':
|
2020-01-10 18:03:18 +01:00
|
|
|
assignment.turk_country = value
|
2020-01-13 13:49:59 +01:00
|
|
|
if name == 'os':
|
|
|
|
assignment.turk_os = value
|
|
|
|
if name == 'browser':
|
|
|
|
assignment.turk_browser = value
|
|
|
|
change = True
|
2020-01-10 18:03:18 +01:00
|
|
|
self.logger.debug(f'Set assignment: {name} to {value}')
|
2020-01-13 13:49:59 +01:00
|
|
|
|
|
|
|
if change:
|
|
|
|
self.store.saveAssignment(assignment)
|
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
elif signal.name == 'server.open':
|
|
|
|
self.currentHit.open_page_at = datetime.datetime.utcnow()
|
|
|
|
self.store.saveHIT(self.currentHit)
|
|
|
|
self.setLight(True)
|
2020-01-22 18:15:47 +01:00
|
|
|
elif signal.name == 'server.close':
|
|
|
|
if not signal.params['abandoned']:
|
|
|
|
continue
|
|
|
|
a = self.currentHit.getLastAssignment()
|
|
|
|
if a.assignment_id != signal.params['assignment_id']:
|
|
|
|
self.logger.info(f"Close of older assignment_id: {signal}")
|
|
|
|
continue
|
|
|
|
self.logger.critical(f"Websocket closed of active assignment_id: {signal}")
|
|
|
|
a.abandoned_at = datetime.datetime.utcnow()
|
|
|
|
self.store.saveAssignment(a)
|
|
|
|
self.plotter.park()
|
2020-01-13 13:49:59 +01:00
|
|
|
elif signal.name == 'assignment.submit':
|
|
|
|
a = self.currentHit.getLastAssignment()
|
|
|
|
if a.assignment_id != signal.params['assignment_id']:
|
|
|
|
self.logger.critical(f"Submit of invalid assignment_id: {signal}")
|
|
|
|
|
|
|
|
a.submit_page_at = datetime.datetime.utcnow()
|
|
|
|
self.store.saveAssignment(a)
|
2019-11-02 22:31:16 +01:00
|
|
|
self.plotter.park()
|
|
|
|
# park always triggers a plotter.finished after being processed
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
elif signal.name[:4] == 'sqs.':
|
|
|
|
if signal.params['event']['HITId'] != self.currentHit.hit_id:
|
|
|
|
self.logger.warning(f"SQS hit.info hit_id != currenthit.id: {signal}, update status for older HIT")
|
|
|
|
sqsHit = self.store.getHitByRemoteId(signal.params['event']['HITId'])
|
|
|
|
updateStatus = False
|
|
|
|
else:
|
|
|
|
sqsHit = self.currentHit
|
|
|
|
updateStatus = True
|
2020-01-13 13:49:59 +01:00
|
|
|
|
|
|
|
sqsAssignment = sqsHit.getAssignmentById(signal.params['event']['AssignmentId'])
|
|
|
|
if not sqsAssignment:
|
|
|
|
self.logger.critical(f"Invalid assignmentId given for hit: {signal.params['event']}")
|
|
|
|
continue
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
if signal.name == 'sqs.AssignmentAccepted':
|
|
|
|
self.logger.info(f'Set status progress to accepted')
|
2020-01-13 13:49:59 +01:00
|
|
|
sqsAssignment.accept_at = datetime.datetime.strptime(signal.params['event']['EventTimestamp'],"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
sqsAssignment.worker_id = signal.params['event']['WorkerId']
|
2019-11-02 22:31:16 +01:00
|
|
|
# {'event': {'HITGroupId': '301G7MYOAJ85NEW128ZDGF5DSBW53S', 'EventType': 'AssignmentAccepted', 'EventTimestamp': '2019-10-23T20:16:10Z', 'HITId': '3IH9TRB0FBAKKZFP3JUD6D9YWQ1I1F', 'AssignmentId': '3BF51CHDTWLN3ZGHRKDUHFKPWIJ0H3', 'WorkerId': 'A1CK46PK9VEUH5', 'HITTypeId': '3EYXOXDEN7RX0YSMN4UMVN01AYKZJ0'}}
|
|
|
|
elif signal.name == 'sqs.AssignmentAbandoned':
|
|
|
|
self.logger.info(f'Set status progress to abandoned')
|
|
|
|
#{'event': {'HITGroupId': '301G7MYOAJ85NEW128ZDGF5DSBW53S', 'EventType': 'AssignmentAbandoned', 'EventTimestamp': '2019-10-23T20:23:06Z', 'HITId': '3JHB4BPSFKKFQ263K4EFULI3LC79QJ', 'AssignmentId': '3U088ZLJVL450PB6MJZUIIUCB6VW0Y', 'WorkerId': 'A1CK46PK9VEUH5', 'HITTypeId': '3EYXOXDEN7RX0YSMN4UMVN01AYKZJ0'}}
|
2020-01-13 13:49:59 +01:00
|
|
|
sqsAssignment.abandoned_at = datetime.datetime.strptime(signal.params['event']['EventTimestamp'],"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
# if updateStatus:
|
|
|
|
# self.setLight(False)
|
2019-11-04 10:36:26 +01:00
|
|
|
self.mturk.create_worker_block(WorkerId=signal.params['event']['WorkerId'], Reason='Accepted task without working on it.')
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
elif signal.name == 'sqs.AssignmentReturned':
|
|
|
|
self.logger.info(f'Set status progress to returned')
|
2020-01-13 13:49:59 +01:00
|
|
|
sqsAssignment.returned_at = datetime.datetime.strptime(signal.params['event']['EventTimestamp'],"%Y-%m-%dT%H:%M:%SZ")
|
2019-11-02 22:31:16 +01:00
|
|
|
# {'event': {'HITGroupId': '301G7MYOAJ85NEW128ZDGF5DSBW53S', 'EventType': 'AssignmentReturned', 'EventTimestamp': '2019-10-23T20:16:47Z', 'HITId': '3IH9TRB0FBAKKZFP3JUD6D9YWQ1I1F', 'AssignmentId': '3BF51CHDTWLN3ZGHRKDUHFKPWIJ0H3', 'WorkerId': 'A1CK46PK9VEUH5', 'HITTypeId': '3EYXOXDEN7RX0YSMN4UMVN01AYKZJ0'}}
|
|
|
|
elif signal.name == 'sqs.AssignmentSubmitted':
|
|
|
|
# {'MessageId': '4b37dfdf-6a12-455d-a111-9a361eb54d88', 'ReceiptHandle': 'AQEBHc0yAdIrEmAV3S8TIoDCRxrItDEvjy0VQko56/Lb+ifszC0gdZ0Bbed24HGHZYr5DSnWkgBJ/H59ZXxFS1iVEH9sC8+YrmKKOTrKvW3gj6xYiBU2fBb8JRq+sEiNSxWLw2waxr1VYdpn/SWcoOJCv6PlS7P9EB/2IQ++rCklhVwV7RfARHy4J87bjk5R3+uEXUUi00INhCeunCbu642Mq4c239TFRHq3mwM6gkdydK+AP1MrXGKKAE1W5nMbwEWAwAN8KfoM1NkkUg5rTSYWmxxZMdVs/QRNcMFKVSf1bop2eCALSoG6l3Iu7+UXIl4HLh+rHp4bc8NoftbUJUii8YXeiNGU3wCM9T1kOerwYVgksK93KQrioD3ee8navYExQRXne2+TrUZUDkxRIdtPGA==', 'MD5OfBody': '01ccb1efe47a84b68704c4dc611a4d8d', 'Body': '{"Events":[{"Answer":"<?xml version=\\"1.0\\" encoding=\\"ASCII\\"?><QuestionFormAnswers xmlns=\\"http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2005-10-01/QuestionFormAnswers.xsd\\"><Answer><QuestionIdentifier>surveycode<\\/QuestionIdentifier><FreeText>test<\\/FreeText><\\/Answer><\\/QuestionFormAnswers>","HITGroupId":"301G7MYOAJ85NEW128ZDGF5DSBW53S","EventType":"AssignmentSubmitted","EventTimestamp":"2019-10-30T08:01:43Z","HITId":"3NSCTNUR2ZY42ZXASI4CS5YWV0S5AB","AssignmentId":"3ZAZR5XV02TTOCBR9MCLCNQV1XKCZL","WorkerId":"A1CK46PK9VEUH5","HITTypeId":"3EYXOXDEN7RX0YSMN4UMVN01AYKZJ0"}],"EventDocId":"34af4cd7f2829216f222d4b6e66f3a3ff9ad8ea6","SourceAccount":"600103077174","CustomerId":"A1CK46PK9VEUH5","EventDocVersion":"2014-08-15"}'}
|
|
|
|
self.logger.info(f'Set status progress to submitted')
|
2020-01-13 13:49:59 +01:00
|
|
|
sqsAssignment.answer = signal.params['event']['Answer']
|
|
|
|
if sqsAssignment.uuid not in sqsAssignment.answer:
|
|
|
|
self.logger.critical(f"Not a valid answer given?! {sqsAssignment.answer}")
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2020-01-13 13:49:59 +01:00
|
|
|
if not sqsAssignment.submit_page_at:
|
2019-11-04 10:36:26 +01:00
|
|
|
# page not submitted, hit is. Nevertheless, create new hit.
|
|
|
|
try:
|
|
|
|
self.mturk.reject_assignment(AssignmentId=signal.params['event']['AssignmentId'], RequesterFeedback='Did not do the assignment')
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.exception(e)
|
|
|
|
self.makeHit()
|
|
|
|
else:
|
2020-01-13 13:49:59 +01:00
|
|
|
sqsAssignment.confirmed_at = datetime.datetime.strptime(signal.params['event']['EventTimestamp'],"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
2020-01-08 17:55:45 +01:00
|
|
|
# block de worker na succesvolle submit, om dubbele workers te voorkomen
|
|
|
|
# TODO: Disabled after worker mail, use quals instead
|
2019-12-18 18:49:07 +01:00
|
|
|
#self.mturk.create_worker_block(WorkerId=signal.params['event']['WorkerId'], Reason='Every worker can only work once on the taks.')
|
|
|
|
#self.logger.warn("Block worker after submission")
|
2019-11-05 10:47:23 +01:00
|
|
|
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
self.store.saveHIT(sqsHit)
|
2020-01-13 13:49:59 +01:00
|
|
|
|
2019-11-02 22:31:16 +01:00
|
|
|
elif signal.name == 'plotter.finished':
|
2020-01-13 13:49:59 +01:00
|
|
|
# is _always_ triggered after submit due to plotter.park()
|
|
|
|
if self.currentHit and self.currentHit.isSubmitted():
|
|
|
|
self.currentHit.plotted_at = datetime.datetime.utcnow()
|
|
|
|
self.store.saveHIT(self.currentHit)
|
|
|
|
|
|
|
|
self.logger.info("Start scan thread")
|
2019-11-02 22:31:16 +01:00
|
|
|
scan = threading.Thread(target=self.scanImage, name='scan')
|
|
|
|
scan.start()
|
2020-01-13 13:49:59 +01:00
|
|
|
elif signal.name == 'plotter.parked':
|
|
|
|
# should this have the code from plotter.finished?
|
|
|
|
pass
|
2019-11-01 20:31:39 +01:00
|
|
|
else:
|
2019-11-02 22:31:16 +01:00
|
|
|
self.logger.critical(f"Unknown signal: {signal.name}")
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.critical(f"Exception on handling {signal}")
|
|
|
|
self.logger.exception(e)
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
def makeHit(self):
|
2020-01-13 13:49:59 +01:00
|
|
|
self.expireCurrentHit() # expire hit if it is there
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2020-01-10 18:03:18 +01:00
|
|
|
self.eventQueue.put(Signal('hit.creating', {'id': self.currentHit.id if self.currentHit else 'start'}))
|
|
|
|
|
2019-11-04 09:24:37 +01:00
|
|
|
self.reloadConfig() # reload new config values if they are set
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-11-04 09:24:37 +01:00
|
|
|
# self.notPaused.wait()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 22:33:37 +02:00
|
|
|
self.currentHit = self.store.createHIT()
|
2019-11-01 17:02:38 +01:00
|
|
|
self.store.currentHit = self.currentHit
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 22:33:37 +02:00
|
|
|
self.logger.info(f"Make HIT {self.currentHit.id}")
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-12-18 18:49:07 +01:00
|
|
|
question = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
<ExternalQuestion xmlns="http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd">
|
|
|
|
<ExternalURL>https://guest.rubenvandeven.com:8888/draw?id={HIT_NR}</ExternalURL>
|
|
|
|
<FrameHeight>0</FrameHeight>
|
|
|
|
</ExternalQuestion>
|
|
|
|
'''.replace("{HIT_NR}",str(self.currentHit.id))
|
2019-11-02 20:45:57 +01:00
|
|
|
estimatedHitDuration = self.store.getEstimatedHitDuration()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
|
|
|
|
2019-11-04 09:24:37 +01:00
|
|
|
# set minimum rate, if they take longer we increase the pay
|
|
|
|
fee = max(self.config['minimum_fee'], (self.config['hour_rate_aim']/3600.) * estimatedHitDuration)
|
2019-10-31 14:35:24 +01:00
|
|
|
self.currentHit.fee = fee
|
2019-10-31 16:04:14 +01:00
|
|
|
self.logger.info(f"Based on average duration of {estimatedHitDuration} fee should be {fee}/hit to get hourly rate of {self.config['hour_rate_aim']}")
|
2019-10-23 10:56:28 +02:00
|
|
|
new_hit = self.mturk.create_hit(
|
|
|
|
Title = 'Trace the drawn line',
|
|
|
|
Description = 'Draw a line over the sketched line in the image',
|
|
|
|
Keywords = 'polygons, trace, draw',
|
2019-10-30 12:44:25 +01:00
|
|
|
Reward = "{:.2f}".format(fee),
|
2019-10-23 10:56:28 +02:00
|
|
|
MaxAssignments = 1,
|
|
|
|
LifetimeInSeconds = self.config['hit_lifetime'],
|
2019-11-02 22:31:16 +01:00
|
|
|
AssignmentDurationInSeconds = self.store.getHitTimeout(),
|
2019-10-23 10:56:28 +02:00
|
|
|
AutoApprovalDelayInSeconds = self.config['hit_autoapprove_delay'],
|
|
|
|
Question = question,
|
|
|
|
)
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 22:33:37 +02:00
|
|
|
self.logger.info(f"Created hit: {new_hit}")
|
2019-11-02 21:23:38 +01:00
|
|
|
if not self.config['for_real']:
|
|
|
|
self.logger.info("https://workersandbox.mturk.com/mturk/preview?groupId=" + new_hit['HIT']['HITGroupId'])
|
|
|
|
else:
|
|
|
|
self.logger.info("https://worker.mturk.com/mturk/preview?groupId=" + new_hit['HIT']['HITGroupId'])
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
self.currentHit.hit_id = new_hit['HIT']['HITId']
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 22:33:37 +02:00
|
|
|
self.store.saveHIT(self.currentHit)
|
2020-01-10 18:03:18 +01:00
|
|
|
# self.server.statusPage.set('hit_id', new_hit['HIT']['HITId'])
|
|
|
|
# self.server.statusPage.set('hit_created', self.currentHit.created_at)
|
|
|
|
# self.server.statusPage.set('fee', f"${self.currentHit.fee:.2f}")
|
|
|
|
# self.server.statusPage.set('state', self.currentHit.getStatus())
|
|
|
|
|
|
|
|
self.eventQueue.put(Signal('hit.created', {'id': self.currentHit.id, 'remote_id': self.currentHit.hit_id}))
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
# mturk.send_test_event_notification()
|
|
|
|
if self.config['amazon']['sqs_url']:
|
|
|
|
notification_info= self.mturk.update_notification_settings(
|
|
|
|
HITTypeId=new_hit['HIT']['HITTypeId'],
|
|
|
|
Notification = {
|
|
|
|
'Destination' : self.config['amazon']['sqs_url'],
|
|
|
|
'Transport': 'SQS',
|
|
|
|
'Version': '2014-08-15',
|
|
|
|
'EventTypes': [
|
|
|
|
'AssignmentAccepted',
|
|
|
|
'AssignmentAbandoned',
|
|
|
|
'AssignmentReturned',
|
|
|
|
'AssignmentSubmitted',
|
|
|
|
# 'AssignmentRejected',
|
|
|
|
# 'AssignmentApproved',
|
|
|
|
# 'HITCreated',
|
|
|
|
# 'HITExpired',
|
|
|
|
# 'HITReviewable',
|
|
|
|
# 'HITExtended',
|
|
|
|
# 'HITDisposed',
|
|
|
|
# 'Ping',
|
|
|
|
]
|
|
|
|
},
|
|
|
|
Active=True
|
|
|
|
)
|
|
|
|
self.logger.debug(notification_info)
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-30 12:44:25 +01:00
|
|
|
def cleanDrawing(self):
|
2019-11-01 17:18:28 +01:00
|
|
|
with self.scanLock:
|
|
|
|
self.eventQueue.put(Signal('scan.start'))
|
|
|
|
# Scan to reset
|
|
|
|
cmd = [
|
2019-11-03 16:11:34 +01:00
|
|
|
'sudo', 'scanimage', '-d', 'epkowa','--resolution=100',
|
|
|
|
'-l','25' #y axis, margin from top of the scanner, hence increasing this, moves the scanned image upwards
|
|
|
|
,'-t','22', # x axis, margin from left side scanner (seen from the outside)
|
2020-01-10 18:03:18 +01:00
|
|
|
'-x',str(181),
|
|
|
|
'-y',str(245)
|
2019-11-01 17:18:28 +01:00
|
|
|
]
|
|
|
|
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
|
|
# opens connection to scanner, but only starts scanning when output becomes ready:
|
|
|
|
_, e = proc.communicate(80)
|
2019-11-01 20:31:39 +01:00
|
|
|
time.sleep(5) # sleep a few seconds for scanner to return to start position
|
2019-10-30 12:44:25 +01:00
|
|
|
if e:
|
|
|
|
self.logger.critical(f"Scanner caused: {e.decode()}")
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-30 12:44:25 +01:00
|
|
|
self.eventQueue.put(Signal('system.reset'))
|
|
|
|
self.eventQueue.put(Signal('scan.finished'))
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-30 12:44:25 +01:00
|
|
|
def reset(self) -> str:
|
2019-11-02 20:45:57 +01:00
|
|
|
self.plotter.park()
|
2019-11-04 08:57:42 +01:00
|
|
|
# Very confusing to have scanning on a reset (because often nothing has happened), so don't do itd
|
|
|
|
# scan = threading.Thread(target=self.cleanDrawing, name='reset')
|
|
|
|
# scan.start()
|
2019-11-02 22:34:44 +01:00
|
|
|
self.server.statusPage.clearAssignment()
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-23 10:56:28 +02:00
|
|
|
def scanImage(self) -> str:
|
|
|
|
"""
|
|
|
|
Run scanimage on scaner and returns a string with the filename
|
|
|
|
"""
|
2020-01-13 13:49:59 +01:00
|
|
|
|
2019-11-01 17:18:28 +01:00
|
|
|
with self.scanLock:
|
2020-01-13 13:49:59 +01:00
|
|
|
if self.config['dummy_plotter']:
|
|
|
|
self.eventQueue.put(Signal('hit.scan', {'id':self.currentHit.id}))
|
|
|
|
self.eventQueue.put(Signal('scan.start'))
|
|
|
|
self.logger.warning("Fake scanner for a few seconds")
|
|
|
|
for i in tqdm.tqdm(range(5)):
|
|
|
|
time.sleep(1)
|
|
|
|
self.eventQueue.put(Signal('hit.scanned', {'hit_id':self.currentHit.id}))
|
|
|
|
self.eventQueue.put(Signal('scan.finished'))
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
cmd = [
|
|
|
|
'sudo', 'scanimage', '-d', 'epkowa', '--format', 'tiff',
|
|
|
|
'--resolution=100', # lower res, faster (more powerful) scan & wipe
|
|
|
|
'-l','25' #y axis, margin from top of the scanner, hence increasing this, moves the scanned image upwards
|
|
|
|
,'-t','22', # x axis, margin from left side scanner (seen from the outside)
|
|
|
|
'-x',str(181),
|
|
|
|
'-y',str(245)
|
|
|
|
]
|
|
|
|
self.logger.info(f"{cmd}")
|
|
|
|
filename = self.currentHit.getImagePath()
|
|
|
|
|
2020-01-10 18:03:18 +01:00
|
|
|
self.eventQueue.put(Signal('hit.scan', {'id':self.currentHit.id}))
|
2019-11-01 17:18:28 +01:00
|
|
|
self.eventQueue.put(Signal('scan.start'))
|
|
|
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
# opens connection to scanner, but only starts scanning when output becomes ready:
|
|
|
|
o, e = proc.communicate(80)
|
2020-01-13 13:49:59 +01:00
|
|
|
if e:
|
|
|
|
self.logger.critical(f"Scanner caused: {e.decode()}")
|
|
|
|
# Should this clear self.isRunning.clear() ?
|
|
|
|
|
|
|
|
try:
|
|
|
|
f = io.BytesIO(o)
|
|
|
|
img = Image.open(f)
|
|
|
|
img = img.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM)
|
|
|
|
tunedImg = Level.level_image(img, self.config['level']['min'], self.config['level']['max'], self.config['level']['gamma'])
|
|
|
|
tunedImg.save(filename)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.critical("Cannot create image from scan. Did scanner work?")
|
|
|
|
self.logger.exception(e)
|
|
|
|
copyfile('www/basic.svg', filename)
|
|
|
|
|
|
|
|
time.sleep(5) # sleep a few seconds for scanner to return to start position
|
|
|
|
|
|
|
|
self.eventQueue.put(Signal('hit.scanned', {'hit_id':self.currentHit.id}))
|
|
|
|
self.eventQueue.put(Signal('scan.finished'))
|
2020-01-08 17:55:45 +01:00
|
|
|
|
2019-10-30 13:31:21 +01:00
|
|
|
def setLight(self, on):
|
|
|
|
value = 1 if on else 0
|
2020-01-13 13:49:59 +01:00
|
|
|
|
|
|
|
if self.lightStatus == value:
|
|
|
|
return
|
|
|
|
self.lightStatus = value
|
|
|
|
|
2019-10-30 13:31:21 +01:00
|
|
|
cmd = [
|
|
|
|
'usbrelay', f'HURTM_1={value}'
|
|
|
|
]
|
|
|
|
self.logger.info(f"Trigger light {cmd}")
|
|
|
|
code = subprocess.call(cmd)
|
|
|
|
if code > 0:
|
|
|
|
self.logger.warning(f"Error on light change: {code}")
|
2020-01-13 13:49:59 +01:00
|
|
|
|
|
|
|
def updateLightHook(self, hit = None):
|
|
|
|
# ignore hit attribute, which comes from the HITstore
|
|
|
|
self.setLight(self.getLightStatus())
|
|
|
|
|
|
|
|
def getLightStatus(self) -> bool:
|
|
|
|
if not self.currentHit:
|
|
|
|
return False
|
|
|
|
a = self.currentHit.getLastAssignment()
|
|
|
|
if not a:
|
|
|
|
return False
|
|
|
|
if self.currentHit.plotted_at: # wait till plotter is done
|
|
|
|
return False
|
|
|
|
return True
|