From 58df48cd914b0fc4f9b66b03c7c603470c5e7faf Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Sun, 17 Oct 2021 10:24:02 +0200 Subject: [PATCH] Prep as service, with buttons to start --- sorteerhoed.py | 5 ++ sorteerhoed/central_management.py | 60 ++++++++++++++++++++- sorteerhoed/plotter.py | 87 ++++++++++++++++++++++--------- sorteerhoed/webserver.py | 86 ++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 28 deletions(-) diff --git a/sorteerhoed.py b/sorteerhoed.py index bd9ae11..918b57c 100644 --- a/sorteerhoed.py +++ b/sorteerhoed.py @@ -21,6 +21,11 @@ if __name__ == '__main__': action='store_true', 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( '--for-real', action='store_true', diff --git a/sorteerhoed/central_management.py b/sorteerhoed/central_management.py index ae6ac26..7ac99a3 100644 --- a/sorteerhoed/central_management.py +++ b/sorteerhoed/central_management.py @@ -140,7 +140,7 @@ class CentralManagement(): # 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']) + # 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( @@ -171,7 +171,8 @@ class CentralManagement(): dispatcherThread = threading.Thread(target=self.eventListener, name='dispatcher') 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(): time.sleep(.5) @@ -385,6 +386,13 @@ class CentralManagement(): elif signal.name == 'plotter.parked': # should this have the code from plotter.finished? 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: self.logger.critical(f"Unknown signal: {signal.name}") except Exception as e: @@ -501,6 +509,54 @@ class CentralManagement(): # scan.start() 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: """ Run scanimage on scaner and returns a string with the filename diff --git a/sorteerhoed/plotter.py b/sorteerhoed/plotter.py index 0d24760..357b603 100644 --- a/sorteerhoed/plotter.py +++ b/sorteerhoed/plotter.py @@ -33,32 +33,62 @@ class Plotter: absPlotWidth = 26.5/2.54 topLeft = absPlotWidth / self.plotWidth #ignore changes in config plotwidth 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): try: if not self.config['dummy_plotter']: self.ad = axidraw.AxiDraw() - - 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) + self.connect() else: self.ad = None + while True: + self.logger.info("Fake AD-connect issue") + time.sleep(1) self.axiDrawCueListener() except Exception as e: self.logger.exception(e) @@ -140,18 +170,23 @@ class Plotter: #if self.goPark: # print("seg",segment) - # 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 + if segment == "disable_motors": + self.disable_motors() + elif segment == "reconnect": + self.reconnect() + else: + # 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 - self.setPenDown(segment[2] == 1) + # and change pen positions + self.setPenDown(segment[2] == 1) - segments.append(segment) + segments.append(segment) except queue.Empty as e: self.logger.debug("Timeout queue.") diff --git a/sorteerhoed/webserver.py b/sorteerhoed/webserver.py index 1cfa0a9..ace4874 100644 --- a/sorteerhoed/webserver.py +++ b/sorteerhoed/webserver.py @@ -231,6 +231,86 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler): client.abandoned = True 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): CORS_ORIGINS = ['localhost'] @@ -521,6 +601,12 @@ class Server: 'eventQ': self.eventQ, '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"/draw", DrawPageHandler, dict(