Prep as service, with buttons to start

This commit is contained in:
Ruben van de Ven 2021-10-17 10:24:02 +02:00
parent ac0176b228
commit 58df48cd91
4 changed files with 210 additions and 28 deletions

View file

@ -21,6 +21,11 @@ if __name__ == '__main__':
action='store_true', action='store_true',
help='Skip attempt to connect to plotter' help='Skip attempt to connect to plotter'
) )
argParser.add_argument(
'--autostart',
action='store_true',
help='Don\'t require a visit to the control panel to start the first hit. Usefull when you know plotter & scanner are already set up'
)
argParser.add_argument( argParser.add_argument(
'--for-real', '--for-real',
action='store_true', action='store_true',

View file

@ -140,7 +140,7 @@ class CentralManagement():
# clear any pending hits: # clear any pending hits:
pending_hits = self.mturk.list_hits(MaxResults=100) pending_hits = self.mturk.list_hits(MaxResults=100)
for pending_hit in pending_hits['HITs']: for pending_hit in pending_hits['HITs']:
# print(pending_hit['HITId'], pending_hit['HITStatus']) # print(pending_hit['HITId'], pending_hit['HITStatus'])
if pending_hit['HITStatus'] == 'Assignable': if pending_hit['HITStatus'] == 'Assignable':
self.logger.warn(f"Expire stale hit: {pending_hit['HITId']}: {pending_hit['HITStatus']}") self.logger.warn(f"Expire stale hit: {pending_hit['HITId']}: {pending_hit['HITStatus']}")
self.mturk.update_expiration_for_hit( self.mturk.update_expiration_for_hit(
@ -171,7 +171,8 @@ class CentralManagement():
dispatcherThread = threading.Thread(target=self.eventListener, name='dispatcher') dispatcherThread = threading.Thread(target=self.eventListener, name='dispatcher')
dispatcherThread.start() dispatcherThread.start()
self.eventQueue.put(Signal('start', {'ding':'test'})) if self.args.autostart:
self.eventQueue.put(Signal('start', {'ding':'test'}))
while self.isRunning.is_set(): while self.isRunning.is_set():
time.sleep(.5) time.sleep(.5)
@ -385,6 +386,13 @@ class CentralManagement():
elif signal.name == 'plotter.parked': elif signal.name == 'plotter.parked':
# should this have the code from plotter.finished? # should this have the code from plotter.finished?
pass pass
elif signal.name == 'scan.test':
self.logger.info("Start test scan thread")
if self.currentHit:
self.logger.error("cannot scan when HITs are already running")
else:
scan = threading.Thread(target=self.scanTestImage, name='scantest')
scan.start()
else: else:
self.logger.critical(f"Unknown signal: {signal.name}") self.logger.critical(f"Unknown signal: {signal.name}")
except Exception as e: except Exception as e:
@ -501,6 +509,54 @@ class CentralManagement():
# scan.start() # scan.start()
self.server.statusPage.clearAssignment() self.server.statusPage.clearAssignment()
def scanTestImage(self) -> str:
"""
Run scanimage on scaner and returns a string with the filename
"""
with self.scanLock:
if self.config['dummy_plotter']:
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('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 = "/tmp/testscan.jpg"
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)
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,quality=95)
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('scan.finished'))
def scanImage(self) -> str: def scanImage(self) -> str:
""" """
Run scanimage on scaner and returns a string with the filename Run scanimage on scaner and returns a string with the filename

View file

@ -33,32 +33,62 @@ class Plotter:
absPlotWidth = 26.5/2.54 absPlotWidth = 26.5/2.54
topLeft = absPlotWidth / self.plotWidth #ignore changes in config plotwidth topLeft = absPlotWidth / self.plotWidth #ignore changes in config plotwidth
self.q.put([(1/2.54)/absPlotWidth + topLeft,0,0]) self.q.put([(1/2.54)/absPlotWidth + topLeft,0,0])
def disable_motors(self):
self.ad.plot_setup()
self.setPenDown(False)
self.ad.options.mode = "manual"
self.ad.options.manual_cmd = "disable_xy"
self.ad.plot_run()
def connect(self):
# back to default control mode:
self.ad.options.mode = "plot"
# connect/disconnect once because often first connection attempt fails
self.ad.interactive()
self.ad.connect()
self.ad.pen_raise()
self.ad.disconnect()
# start?
self.ad.interactive()
connected = self.ad.connect()
# if not connected:
# raise Exception("Cannot connect to Axidraw")
while not connected:
self.logger.error("Cannot connect to Axidraw (retry, 1s)")
self.ad.disconnect()
time.sleep(1)
connected = self.ad.connect()
# self.ad.options.units = 1 # set to use centimeters instead of inches
self.ad.options.accel = 100;
self.ad.options.speed_penup = 100
self.ad.options.speed_pendown = 100
self.ad.options.model = 1 # 2 for A3, 1 for A4
self.ad.options.pen_pos_up = 100
def reconnect(self):
self.connect()
# no park on intial connection
self.park()
# self.ad.moveto(0,0)
def start(self): def start(self):
try: try:
if not self.config['dummy_plotter']: if not self.config['dummy_plotter']:
self.ad = axidraw.AxiDraw() self.ad = axidraw.AxiDraw()
self.connect()
self.ad.interactive()
# self.ad.plot_path()
connected = self.ad.connect()
if not connected:
raise Exception("Cannot connect to Axidraw")
# self.ad.options.units = 1 # set to use centimeters instead of inches
self.ad.options.accel = 100;
self.ad.options.speed_penup = 100
self.ad.options.speed_pendown = 100
self.ad.options.model = 1 # 2 for A3, 1 for A4
self.ad.options.pen_pos_up = 100
self.park()
# self.ad.moveto(0,0)
else: else:
self.ad = None self.ad = None
while True:
self.logger.info("Fake AD-connect issue")
time.sleep(1)
self.axiDrawCueListener() self.axiDrawCueListener()
except Exception as e: except Exception as e:
self.logger.exception(e) self.logger.exception(e)
@ -140,18 +170,23 @@ class Plotter:
#if self.goPark: #if self.goPark:
# print("seg",segment) # print("seg",segment)
# change of pen state? draw previous segments! if segment == "disable_motors":
if (segment[2] == 1 and not self.pen_down) or (segment[2] == 0 and self.pen_down) or len(segments) > 150: self.disable_motors()
if len(segments): elif segment == "reconnect":
self.draw_segments(segments) self.reconnect()
plotterRan = True else:
segments = [] #reset # change of pen state? draw previous segments!
if (segment[2] == 1 and not self.pen_down) or (segment[2] == 0 and self.pen_down) or len(segments) > 150:
if len(segments):
self.draw_segments(segments)
plotterRan = True
segments = [] #reset
# and change pen positions # and change pen positions
self.setPenDown(segment[2] == 1) self.setPenDown(segment[2] == 1)
segments.append(segment) segments.append(segment)
except queue.Empty as e: except queue.Empty as e:
self.logger.debug("Timeout queue.") self.logger.debug("Timeout queue.")

View file

@ -231,6 +231,86 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
client.abandoned = True client.abandoned = True
client.close() client.close()
class ConfigWebSocketHandler(tornado.websocket.WebSocketHandler):
"""
Websocket for config & control
"""
CORS_ORIGINS = ['localhost', 'guest.rubenvandeven.com']
connections = set()
def initialize(self, config, plotterQ: Queue, eventQ: Queue, store: HITStore):
self.config = config
self.plotterQ = plotterQ
self.eventQ = eventQ
self.store = store
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])
logger.info(f"Connection from {origin} is valid? {valid=}")
return valid
# the client connected
def open(self, p = None):
self.__class__.connections.add(self)
logger.warning(f"Config client connected: {self.request.remote_ip}")
#logger.info(f"New client connected: {self.request.remote_ip} for {self.hit.id}/{self.hit.hit_id}")
# self.eventQ.put(Signal('server.open', dict(assignment_id=self.assignment_id)))
# self.strokes = []
# the client sent the message
def on_message(self, message):
logger.debug(f"recieve: {message}")
try:
msg = json.loads(message)
if msg['action'] == 'start':
self.eventQ.put(Signal('start', {'ding':'test'}))
elif msg['action'] == 'disable_motors':
self.plotterQ.put("disable_motors")
elif msg['action'] == 'enable_motors':
self.plotterQ.put("reconnect")
elif msg['action'] == 'scanner_test':
self.eventQ.put(Signal('scan.test'))
# elif msg['action'] == 'info':
# self.eventQ.put(Signal('assignment.info', dict(
# hit_id=self.hit.id,
# assignment_id=self.assignment_id,
# resolution=msg['resolution'],
# browser=msg['browser']
# )))
# 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.warning(f"Config client disconnected: {self.request.remote_ip}")
# TODO: abandon assignment??
@classmethod
def rmConnection(cls, client):
if client not in cls.connections:
return
cls.connections.remove(client)
@classmethod
def hasConnection(cls, client):
return client in cls.connections
class StatusWebSocketHandler(tornado.websocket.WebSocketHandler): class StatusWebSocketHandler(tornado.websocket.WebSocketHandler):
CORS_ORIGINS = ['localhost'] CORS_ORIGINS = ['localhost']
@ -521,6 +601,12 @@ class Server:
'eventQ': self.eventQ, 'eventQ': self.eventQ,
'store': self.store, 'store': self.store,
}), }),
(r"/config/ws(.*)", ConfigWebSocketHandler, {
'config': self.config,
'plotterQ': self.plotterQ,
'eventQ': self.eventQ,
'store': self.store,
}),
(r"/status/ws", StatusWebSocketHandler, dict(statusPage = self.statusPage)), (r"/status/ws", StatusWebSocketHandler, dict(statusPage = self.statusPage)),
(r"/draw", DrawPageHandler, (r"/draw", DrawPageHandler,
dict( dict(