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',
|
||||
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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -34,31 +34,61 @@ class Plotter:
|
|||
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.")
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue