Prep as service, with buttons to start
This commit is contained in:
parent
ac0176b228
commit
58df48cd91
4 changed files with 210 additions and 28 deletions
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue