def __init__(self, app): self.app = app self.screen = app.screen self.widgets = [] # although 1 would be ideal, this will drift and thus we will have some # values twice which does not look good. # TODO: only re-render widgets which need periodic refreshing self.periodic_refresh_timer = WaitableTimer(self.app.sel, 0.25, self.render, True) self.periodic_refresh_timer.start() self.last_update = None
def accept(self, sock): conn, addr = sock.accept() # Should be ready log.info("accepted connection from {}\n".format(addr)) conn.setblocking(False) self.selector.register(conn, selectors.EVENT_READ, self.read) self.data_buffer[conn] = PacketParser( WaitableTimer(self.selector, NETWORK_DELAY, None)) self.connections.append(conn)
def __init__(self, app) -> None: super(ShootingRange, self).__init__(app, Vector(0,0), Vector(102,37)) self.center_in(app.screen) self.state = ShootingRangeState.READY try: self.target = ReddotTarget() #("/dev/ttyUSB2") self.target.start() # start asynchronous thread which polls the target except ReddotTarget.AutodetectFailedException as e: log.error(str(e)) self.target = None self.state = ShootingRangeState.NOT_WORKING self.target_rect_size = Vector(70,33) self.target_point_diam = Vector(len(TARGET_CIRCLE[0]), len(TARGET_CIRCLE)) self.MAX_POS = 4500 # min/max X/Y coordinate returned by reddot target self.CIRCLE_RAD = 1500 cfg = app.cfg["shooting_range"] self.TIMEOUT = cfg["timeout"] self.MAX_BONUS_TIME = cfg["max_bonus_time"] self.MAX_POINTS_FOR_BONUS = cfg["max_points_for_bonus"] curses.init_pair(50, curses.COLOR_BLACK, curses.COLOR_WHITE) # bg white curses.init_pair(51, curses.COLOR_WHITE, curses.COLOR_RED) # bg red curses.init_pair(52, curses.COLOR_WHITE, curses.COLOR_BLACK) # point white curses.init_pair(53, curses.COLOR_BLACK, curses.COLOR_GREEN) # point red curses.init_pair(54, curses.COLOR_YELLOW, curses.COLOR_BLACK) # points total self.closed_callback = None self.first_shot_callback = None self.time_started = 0 self.shots = [] # type: List[Tuple[Vector, float, float]] self.timeout_timer = WaitableTimer(self.app.sel, self.TIMEOUT, self.shooting_range_timeout)
class ShootingRange(WidgetBase): def __init__(self, app) -> None: super(ShootingRange, self).__init__(app, Vector(0,0), Vector(102,37)) self.center_in(app.screen) self.state = ShootingRangeState.READY try: self.target = ReddotTarget() #("/dev/ttyUSB2") self.target.start() # start asynchronous thread which polls the target except ReddotTarget.AutodetectFailedException as e: log.error(str(e)) self.target = None self.state = ShootingRangeState.NOT_WORKING self.target_rect_size = Vector(70,33) self.target_point_diam = Vector(len(TARGET_CIRCLE[0]), len(TARGET_CIRCLE)) self.MAX_POS = 4500 # min/max X/Y coordinate returned by reddot target self.CIRCLE_RAD = 1500 cfg = app.cfg["shooting_range"] self.TIMEOUT = cfg["timeout"] self.MAX_BONUS_TIME = cfg["max_bonus_time"] self.MAX_POINTS_FOR_BONUS = cfg["max_points_for_bonus"] curses.init_pair(50, curses.COLOR_BLACK, curses.COLOR_WHITE) # bg white curses.init_pair(51, curses.COLOR_WHITE, curses.COLOR_RED) # bg red curses.init_pair(52, curses.COLOR_WHITE, curses.COLOR_BLACK) # point white curses.init_pair(53, curses.COLOR_BLACK, curses.COLOR_GREEN) # point red curses.init_pair(54, curses.COLOR_YELLOW, curses.COLOR_BLACK) # points total self.closed_callback = None self.first_shot_callback = None self.time_started = 0 self.shots = [] # type: List[Tuple[Vector, float, float]] self.timeout_timer = WaitableTimer(self.app.sel, self.TIMEOUT, self.shooting_range_timeout) #for i in range(25): #self.shots.append( #(Vector(1,1), 10, 42) #) ## all the following shots are within the red dot #self.shots.extend([ # (Vector(452, -1400), 1306.6, 5.7), # (Vector(452, -1260), 1306.6, 5.7), # (Vector(411, -1424), 1482.1, 5.0), # (Vector(-720, -926), 1172.9, 6.3), # (Vector(-786, 648), 1018.6, 6.9), # (Vector(976, 601), 1146.2, 6.4), # (Vector(937, 922), 1314.5, 5.7), # (Vector(-1005, 799), 1283.9, 5.8), # (Vector(-36, 1031), 1031.6, 6.8), # (Vector(1168, 152), 1177.8, 6.2), # (Vector(-1333, 129), 1339.2, 5.6), # (Vector(0, 0), 0, 0), #]) def reset(self): self.shots = [] self.time_started = 0 if self.state != ShootingRangeState.NOT_WORKING: self.state = ShootingRangeState.READY self.timeout_timer.reset(self.TIMEOUT) def handle_input(self, key): if not self.app.cfg["cheats"]: return # insert random shot if key == curses.KEY_F5: pts = random.randint(0,100)/10 dist = random.randint(0,1000) px = random.randint(-self.MAX_POS, self.MAX_POS) py = random.randint(-self.MAX_POS, self.MAX_POS) self.handle_shot([0,0,0,0,0,0,pts,dist,px,py]) def handle_shot(self, shot): log.debug("handling shot: shooting range state = {}".format(self.state)) if self.state == ShootingRangeState.READY: log.debug("received first shot! callback = {}".format(self.first_shot_callback)) self.timeout_timer.start() self.state = ShootingRangeState.ACTIVE self.time_started = time.time() self.first_shot_callback() elif self.state == ShootingRangeState.DISABLED: # we are disabled and thus we discard all future shots return pos = Vector(int(shot[8]), -int(shot[9])) # y axis is inverted relative to screen dist = float(shot[7]) points = float(shot[6]) log.info("handling shot: Pos={}, Dist={}, points={}".format(pos, dist, points)) self.shots.append( (pos, dist, points) ) def draw(self) -> None: self.screen.erase() self.screen.border() # render target square for l in range(self.target_rect_size.y): self.screen.addstr(l+2, 2, " "*self.target_rect_size.x, curses.color_pair(50)) # render target circle o = Vector(2,0)+self.target_rect_size/2-self.target_point_diam/2 for line, text in enumerate(TARGET_CIRCLE): w = len([c for c in text if c != ' ']) self.screen.addstr(int(line+o.y), int(o.x+self.target_point_diam.x/2-w/2), " "*w, curses.color_pair(51)) self.screen.addstr(2, self.target_rect_size.x+4, "Verbleibende Zeit: {}".format(self.remaining_time()), curses.A_NORMAL) self.screen.addstr(4, self.target_rect_size.x+4, "Treffer:", curses.A_BOLD) self.screen.addstr(5, self.target_rect_size.x+4, "Nr.: Punkte.:", curses.A_BOLD) self.screen.addstr(self.target_rect_size.y-2, self.target_rect_size.x+4, "Total:", curses.A_BOLD) self.screen.addstr(self.target_rect_size.y-2, self.target_rect_size.x+9, "{:>13}".format(self.total_points()), curses.A_BOLD + curses.color_pair(54)) self.screen.addstr(self.target_rect_size.y-1, self.target_rect_size.x+4, "Bonuszeit:", curses.A_BOLD) self.screen.addstr(self.target_rect_size.y-1, self.target_rect_size.x+15, time_format(self.points_to_bonus_time()), curses.A_BOLD + curses.color_pair(54)) list_offset = max(len(self.shots) - self.target_rect_size.y + 7, 0) for nr, (pos, dist, pts) in enumerate(self.shots): p = (pos/(self.MAX_POS*2) + Vector(0.5, 0.5))*self.target_rect_size #log.info("drawing shot {} to {}".format(pos, p)) # highlight last shot if nr == len(self.shots)-1: tbl_flags = curses.A_STANDOUT if dist <= self.CIRCLE_RAD: pt_flags = curses.color_pair(53) else: pt_flags = curses.color_pair(52) else: tbl_flags = curses.A_NORMAL if dist <= self.CIRCLE_RAD: pt_flags = curses.color_pair(51) else: pt_flags = curses.color_pair(50) self.screen.addstr(int(p.y), int(p.x), 'X', pt_flags) # don't draw more shots into table than there is space if nr < list_offset: continue self.screen.addstr(nr+6-list_offset, self.target_rect_size.x+4, " {:>3}. {:>12} ".format(nr+1, pts), tbl_flags) self.screen.addstr(1,2, 'Schiessstand', curses.A_BOLD) #self.screen.addstr(2,2, str(self.target.ser.port)) def shooting_range_timeout(self): log.info("shooting range timeout") self.shooting_range_state = ShootingRangeState.DISABLED self.app.widget_mgr.show_popup( 'Schiessstand beendet', 'Sie haben {} Bonuszeit erhalten'.format(time_format(self.points_to_bonus_time())), self.closed_callback, ['OK']) # force render as we did not receive a regular input self.app.widget_mgr.render() def total_points(self): return int(sum([p for _, _, p in self.shots])) def points_to_bonus_time(self): total = min(self.total_points(), self.MAX_POINTS_FOR_BONUS) return total / self.MAX_POINTS_FOR_BONUS * self.MAX_BONUS_TIME def remaining_time(self): diff = max(math.ceil(self.time_started + self.TIMEOUT - time.time()), 0) return time_format(diff)
def __init__(self, screen): self.sel = selectors.DefaultSelector() self.screen = screen self.is_running = False # optionally pass different configuration file (useful for debugging) if len(sys.argv) > 1: app_cfg_path = sys.argv[1] else: app_cfg_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'app.cfg') with open(app_cfg_path, 'r') as app_cfg: self.cfg = json.load(app_cfg) log.info("using application configuration: {}".format(self.cfg)) if not self.cfg['cheats']: # ignore CTRL+Z/V keybinding log.info("disabling SIGINT") signal.signal(signal.SIGINT, signal.SIG_IGN) #curses.raw() # disable special keys and stuff (such as ctrl-c) self.screen.nodelay(True) # disable blocking on getch() curses.curs_set(False) # hide cursor curses.mousemask(curses.ALL_MOUSE_EVENTS) # enable mouse interaction curses.nonl() # don't translate KEY_RETURN into '\r\n' self.screen.clear() # make sure display is big enough H, W = self.screen.getmaxyx() log.info("Screen Size: {} cols by {} rows".format(W, H)) if W < 102 or H < 39: raise Exception( "Screen is too small: {} rows by {} columns, but we need at least 39 by 102.\n" .format(H, W) + "Try a smaller font size or increasing the resolution of your terminal emulator." ) # create server for remote control self.mi = ManagementInterface(1234, self.sel) log.info("local address: {}".format(self.mi.get_local_addresses())) self.mi.register_handler('quit', self.exit_app_by_packet) self.mi.register_handler('shutdown', self.shutdown) self.mi.register_handler('reset', lambda _: self.reset()) self.mi.register_handler('show_popup', self.show_popup_from_packet) self.mi.register_handler('ping', lambda _: None) # dummy command self.mi.register_handler('set_time', lambda p: self.set_timeout(p['timeout'])) self.mi.register_handler('show_shooting_range', lambda _: self.show_shooting_range()) self.mi.register_handler( 'get_time', lambda p: self.mi.reply_success(p, self.remaining_time())) self.mi.register_handler('restore_saved_state', self.restore_backup_by_packet) self.mi.register_handler('memory_dump', lambda p: self.memory_dump()) self.mi.register_handler('wakeup', lambda p: self.wakeup_screen()) self.mi.new_connection_handler = self.new_connection_handler self.widget_mgr = WidgetManager(self) self.TIMEOUT = self.cfg["game_timeout"] #60*60 self.timeout_timer = WaitableTimer(self.sel, self.TIMEOUT, self.on_timeout) self.set_timeout(self.TIMEOUT) # register key handler self.sel.register(sys.stdin, selectors.EVENT_READ, self.handle_input) puzzle_filename = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'puzzle.cfg') with open(puzzle_filename, 'r', encoding="utf-8") as puzzle_cfg: pcfg = json.load(puzzle_cfg) log.info("using puzzle configuration: {}".format(pcfg)) h, w = screen.getmaxyx() self.puzzle = Crossword(self, Vector(10, 1), Vector(w - 10, h - 2), pcfg) self.widget_mgr.show(self.puzzle) self.puzzle.resize_to_contents() H, W = self.puzzle.screen.getmaxyx() log.info("Puzzle Size: {} x {}".format(W, H)) # center in middle of screen (also take progress bar into account) ps = self.puzzle.size() try: self.puzzle.move( Vector(int((w - ps.x) / 2), int((h - ps.y + 6) / 2))) except Exception as e: raise Exception( "Failed to move curses window. This can happen if your screen is too small!\nMaybe you forgot to switch to fullscreen?" ) self.progress_bar = ProgressBar(self, self.puzzle.pos() - Vector(0, 5), Vector(self.puzzle.size().x, 5), 'Lösungsfortschritt:') self.puzzle.progress_bar = self.progress_bar self.puzzle.solved_callback = self.on_crossword_solved self.door_panel = DoorPanel(self) self.door_panel.center_in(self.screen) self.door_panel.visible = False #self.widget_mgr.add(self.door_panel) self.shooting_range = ShootingRange(self) self.shooting_range.first_shot_callback = self.show_shooting_range self.shooting_range.closed_callback = self.on_shooting_range_closed if self.shooting_range.target is None: self.widget_mgr.show_popup( "Hallo", "Konnte keine Verbindung zum Reddot-Target aufbauen. " "Schiessstand ist deaktiviert.") else: self.sel.register(self.shooting_range.target.shots_queue_available, selectors.EVENT_READ, self.handle_shot) self.sel.register(self.shooting_range.target.has_raised_exception, selectors.EVENT_READ, self.handle_exception_in_reddot_target) self.final_screen = FinalScreen(self) self.check_for_backup() self.periodic_backup_timer = WaitableTimer(self.sel, 10, self.on_periodic_backup) self.periodic_backup_timer.start()
class Application: def __init__(self, screen): self.sel = selectors.DefaultSelector() self.screen = screen self.is_running = False # optionally pass different configuration file (useful for debugging) if len(sys.argv) > 1: app_cfg_path = sys.argv[1] else: app_cfg_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'app.cfg') with open(app_cfg_path, 'r') as app_cfg: self.cfg = json.load(app_cfg) log.info("using application configuration: {}".format(self.cfg)) if not self.cfg['cheats']: # ignore CTRL+Z/V keybinding log.info("disabling SIGINT") signal.signal(signal.SIGINT, signal.SIG_IGN) #curses.raw() # disable special keys and stuff (such as ctrl-c) self.screen.nodelay(True) # disable blocking on getch() curses.curs_set(False) # hide cursor curses.mousemask(curses.ALL_MOUSE_EVENTS) # enable mouse interaction curses.nonl() # don't translate KEY_RETURN into '\r\n' self.screen.clear() # make sure display is big enough H, W = self.screen.getmaxyx() log.info("Screen Size: {} cols by {} rows".format(W, H)) if W < 102 or H < 39: raise Exception( "Screen is too small: {} rows by {} columns, but we need at least 39 by 102.\n" .format(H, W) + "Try a smaller font size or increasing the resolution of your terminal emulator." ) # create server for remote control self.mi = ManagementInterface(1234, self.sel) log.info("local address: {}".format(self.mi.get_local_addresses())) self.mi.register_handler('quit', self.exit_app_by_packet) self.mi.register_handler('shutdown', self.shutdown) self.mi.register_handler('reset', lambda _: self.reset()) self.mi.register_handler('show_popup', self.show_popup_from_packet) self.mi.register_handler('ping', lambda _: None) # dummy command self.mi.register_handler('set_time', lambda p: self.set_timeout(p['timeout'])) self.mi.register_handler('show_shooting_range', lambda _: self.show_shooting_range()) self.mi.register_handler( 'get_time', lambda p: self.mi.reply_success(p, self.remaining_time())) self.mi.register_handler('restore_saved_state', self.restore_backup_by_packet) self.mi.register_handler('memory_dump', lambda p: self.memory_dump()) self.mi.register_handler('wakeup', lambda p: self.wakeup_screen()) self.mi.new_connection_handler = self.new_connection_handler self.widget_mgr = WidgetManager(self) self.TIMEOUT = self.cfg["game_timeout"] #60*60 self.timeout_timer = WaitableTimer(self.sel, self.TIMEOUT, self.on_timeout) self.set_timeout(self.TIMEOUT) # register key handler self.sel.register(sys.stdin, selectors.EVENT_READ, self.handle_input) puzzle_filename = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'puzzle.cfg') with open(puzzle_filename, 'r', encoding="utf-8") as puzzle_cfg: pcfg = json.load(puzzle_cfg) log.info("using puzzle configuration: {}".format(pcfg)) h, w = screen.getmaxyx() self.puzzle = Crossword(self, Vector(10, 1), Vector(w - 10, h - 2), pcfg) self.widget_mgr.show(self.puzzle) self.puzzle.resize_to_contents() H, W = self.puzzle.screen.getmaxyx() log.info("Puzzle Size: {} x {}".format(W, H)) # center in middle of screen (also take progress bar into account) ps = self.puzzle.size() try: self.puzzle.move( Vector(int((w - ps.x) / 2), int((h - ps.y + 6) / 2))) except Exception as e: raise Exception( "Failed to move curses window. This can happen if your screen is too small!\nMaybe you forgot to switch to fullscreen?" ) self.progress_bar = ProgressBar(self, self.puzzle.pos() - Vector(0, 5), Vector(self.puzzle.size().x, 5), 'Lösungsfortschritt:') self.puzzle.progress_bar = self.progress_bar self.puzzle.solved_callback = self.on_crossword_solved self.door_panel = DoorPanel(self) self.door_panel.center_in(self.screen) self.door_panel.visible = False #self.widget_mgr.add(self.door_panel) self.shooting_range = ShootingRange(self) self.shooting_range.first_shot_callback = self.show_shooting_range self.shooting_range.closed_callback = self.on_shooting_range_closed if self.shooting_range.target is None: self.widget_mgr.show_popup( "Hallo", "Konnte keine Verbindung zum Reddot-Target aufbauen. " "Schiessstand ist deaktiviert.") else: self.sel.register(self.shooting_range.target.shots_queue_available, selectors.EVENT_READ, self.handle_shot) self.sel.register(self.shooting_range.target.has_raised_exception, selectors.EVENT_READ, self.handle_exception_in_reddot_target) self.final_screen = FinalScreen(self) self.check_for_backup() self.periodic_backup_timer = WaitableTimer(self.sel, 10, self.on_periodic_backup) self.periodic_backup_timer.start() def __del__( self ): # TODO: this should be implemented using a context manager ('with') self.mi.delete() del self.mi self.exit() def set_timeout(self, seconds): if seconds <= 0: log.warn( "set timeout called with invalid timeout '{}', setting timeout to one second." .format(seconds)) seconds = 1 # netagive or zero timeouts are not allowed self.TIMEOUT = seconds self.time_ends = time.time() + self.TIMEOUT self.timeout_timer.reset( self.TIMEOUT ) # stop any running timer (if any) and set new timeout self.timeout_timer.start() def on_timeout(self): log.info("main application timeout!") self.mi.send_packet({ 'event': 'main_timeout', }) self.widget_mgr.remove(self.puzzle) self.widget_mgr.remove(self.shooting_range) self.widget_mgr.remove(self.door_panel) self.widget_mgr.show(self.final_screen) self.screen.clear() self.screen.refresh() self.widget_mgr.show_popup( "Zeit Abgelaufen", "Ihre Zeit ist leider rum. Bitte begeben Sie sich zum Ausgang.\nFreundlichst, Ihre Spielleitung" ) self.widget_mgr.render() self.clear_backup() def show_static_crossword(self): log.info("showing crossword again") self.widget_mgr.remove_all() self.widget_mgr.show(self.puzzle) def handle_input(self, stdin): k = self.screen.get_wch() if k == curses.KEY_F1: self.show_help() elif k == curses.KEY_F12: self.show_about() elif k == curses.KEY_F9: if self.cfg["cheats"]: self.show_admin_screen() else: self.widget_mgr.show_input("Management", "Passwort:", self.show_admin_screen, True) else: if not self.widget_mgr.handle_input(k): log.info("unhandled key '{}'".format(k)) def handle_shot(self, _): self.shooting_range.target.shots_queue_available.clear() while not self.shooting_range.target.shots_queue.empty(): self.shooting_range.handle_shot( self.shooting_range.target.shots_queue.get()) def show_about(self): self.widget_mgr.show_popup( 'Kreuzworträtsel', """Geschrieben von Samuel Bryner: Diese Software ist frei verfügbar unter der GPL. Quellcode unter https://github.com/iliis/crossword """) def show_admin_screen(self, pw=None): if pw == self.cfg["admin_pw"] or self.cfg["cheats"]: ser_port = "not connected" if self.shooting_range.target is not None: ser_port = self.shooting_range.target.ser.name self.widget_mgr.show_popup( 'Admin', 'Version {}\nLast Update: {}\n\n'.format( get_version(), get_version_date()) + 'Serial Port: {}\n\n'.format(ser_port) + 'Local Address:\n{}\n'.format('\n'.join( ' - {}'.format(a) for a in self.mi.get_local_addresses())) + 'Local Port: {}\n\n'.format(self.mi.port) + 'Remote Control Connections:\n{}\n'.format('\n'.join( ' - {}'.format(c.getpeername()) for c in self.mi.connections)), callback=self._admin_screen_cb, buttons=[ 'CLOSE', 'LOAD BACKUP', 'AUTOFILL', 'SHOW SRANGE', 'RESET ALL', 'EXIT APP' ]) else: self.widget_mgr.show_popup( 'Passwort Falsch', 'Die Management-Konsole ist nur für die Spielleitung gedacht, sorry.' ) def _admin_screen_cb(self, button): if button == 'EXIT APP': log.info("Exiting application through admin panel.") self.exit() elif button == 'RESET ALL': self.reset() elif button == 'AUTOFILL': self.puzzle.autofill() elif button == 'SHOW SRANGE': self.show_shooting_range() elif button == 'LOAD BACKUP': if not self.restore_backup(): self.widget_mgr.show_popup( 'Backup wiederhestellen fehlgeschlagen', 'Konnte kein Backup laden. Kontrollier das log für weitere Fehler. Gibt es überhaupt ein Backup?' ) elif button == 'CLOSE': pass # just ignore else: raise ValueError("Unknown button pressed in admin panel: '" + str(button) + "'.") def exit_app_by_packet(self, packet): log.info("Exiting application trough remote command.") self.exit() def exit(self): self.is_running = False # if something fails, shooting_range is not yet created, thus we # trigger another exception when trying to set this variable. To # protect against this, let's check if the member variable exists # before accessing it if hasattr( self, 'shooting_range') and self.shooting_range.target is not None: self.shooting_range.target.is_running = False self.backup_state() self.mi.close() self.mi = None def show_popup_from_packet(self, packet): if not 'title' in packet or not 'text' in packet: raise ValueError( "Invalid command: missing 'title' or 'text' field.") if 'buttons' in packet: buttons = packet['buttons'] else: buttons = ['OK'] self.widget_mgr.show_popup(packet['title'], packet['text'], buttons=buttons) def on_crossword_solved(self, _): self.widget_mgr.remove(self.puzzle) self.widget_mgr.show(self.door_panel) self.backup_state() def show_shooting_range(self): if self.shooting_range.target is not None: if self.shooting_range.state == ShootingRangeState.NOT_WORKING: raise Exception( "Cannot start shooting range: Target is not working.") else: self.widget_mgr.show(self.shooting_range) def on_shooting_range_closed(self, _): self.widget_mgr.remove(self.shooting_range) # convert bonus point to time self.set_timeout(self.remaining_time_in_seconds() + self.shooting_range.points_to_bonus_time()) self.mi.send_packet({ 'event': 'shooting_range_timeout', 'bonus_time': self.shooting_range.points_to_bonus_time(), 'total_points': self.shooting_range.total_points(), 'remaining_time': self.remaining_time_in_seconds() }) self.backup_state() def handle_exception_in_reddot_target(self, _): self.shooting_range.target.has_raised_exception.clear() exc, info = self.shooting_range.target.cached_exception raise exc from None def remaining_time_in_seconds(self): return max(math.ceil(self.time_ends - time.time()), 0) def remaining_time(self): return time_format(self.remaining_time_in_seconds()) def shutdown(self, packet): log.info("Shutting down PC!!") subprocess.call(["sudo", "halt"]) def show_help(self): self.widget_mgr.show_popup( 'Hilfe', 'Für jeden Verbrecher gibt es ein Rätsel. Löse die Rätsel und trage die Lösungsworte hier im Kreuzworträtsel ein um auszubrechen. ' 'Pfeiltasten auf der Tastatur benutzen zum navigieren. Sobald alles korrekt ausgefüllt ist erscheint die Türsteuerung. ' 'Bei Fragen oder wenn Ihr einen Tipp braucht einfach mit dem Telefon anrufen: Kurbeln und Höhrer ans Ohr halten.' ) def reset(self): log.info("Resetting application!") self.screen.clear() self.puzzle.reset() self.door_panel.reset() self.shooting_range.reset() self.widget_mgr.remove_all() self.TIMEOUT = self.cfg["game_timeout"] self.set_timeout(self.TIMEOUT) self.widget_mgr.show(self.puzzle) def run(self): #ser.write(b'crossword running') self.is_running = True while self.is_running: self.widget_mgr.render() events = self.sel.select() for key, mask in events: callback = key.data callback(key.fileobj) def on_periodic_backup(self): self.backup_state() self.periodic_backup_timer.reset() self.periodic_backup_timer.start() def backup_state(self): state = { # Q: store this as an absolute timestamp, so time continues? # A: No, don't count time it took to restart app, that's not the players fault 'time_remaining': self.remaining_time_in_seconds(), # TODO: puzzle state backup/restore should be handled in Crossword class itself 'puzzle_input': [''.join(line) for line in self.puzzle.puzzle_input], 'puzzle_solved': self.door_panel.visible } with open('state_backup.cfg', 'w') as state_file: json.dump(state, state_file) log.info("wrote state to backup file: {}".format(state)) def clear_backup(self): if os.path.exists("state_backup.cfg"): log.info("deleting state backup") os.remove("state_backup.cfg") def load_backup(self): if not os.path.isfile('state_backup.cfg'): log.error("cannot restore state from backup: file does not exist") return None try: with open('state_backup.cfg', 'r') as state_file: return json.load(state_file) except: return None def restore_backup_by_packet(self, packet): if self.restore_backup(): return self.mi.reply_success(packet) else: return self.mi.reply_failure( packet, "Could not restore state. Maybe there is no backup?") def restore_backup(self): state = self.load_backup() if state is None: return False # don't restore a completed game if state["time_remaining"] <= 0: return False self.set_timeout(state["time_remaining"]) if state["puzzle_solved"]: self.widget_mgr.remove(self.puzzle) self.widget_mgr.show(self.door_panel) self.widget_mgr.render(clear=True) else: # TODO: puzzle state backup/restore should be handled in Crossword class itself self.puzzle.puzzle_input = [[c for c in line] for line in state["puzzle_input"]] self.puzzle.notify_state_update('restore') log.warning("Restored state from backup. Remaining time: {}".format( self.remaining_time_in_seconds())) self.mi.send_packet({ 'event': 'backup_restored', 'remaining_time': self.remaining_time_in_seconds(), }) return True def is_backup_different(self): """ Check if there is anything in the backup to be interesting. """ if not os.path.exists('state_backup.cfg'): return False # no backup -> nothing interesting backup = self.load_backup() if not backup: log.error( "Failed to load backup even though file exists?! This should not happen." ) return False if backup["time_remaining"] <= 0: return False if backup["puzzle_solved"]: return True # check crossword puzzle for line_backup, line_puzzle in zip(backup["puzzle_input"], self.puzzle.puzzle_input): if [c for c in line_backup] != line_puzzle: return True # nothing to restore from backup return False def check_for_backup(self): # check if backup contains anything interesting if self.is_backup_different(): # if so, ask if you want to restore it self.widget_mgr.show_popup( 'Wiederherstellen?', 'Offenbar wurde das Spiel unterbrochen und nicht korrekt zu Ende gespielt.\n' 'Dies sollte nicht passieren, bitte melden Sie diesen Vorfall der Spielleitung.\n\n' 'Möchten Sie Ihren den vorherigen Zustand wieder rekonstruieren? Andernfalls müssen Sie mit dem Kreuzworträtsel von vorne beginnen.', callback=self._check_backup_popup_cb, buttons=['WIEDERHERSTELLEN', 'NEUES SPIEL']) def _check_backup_popup_cb(self, button): if button == 'WIEDERHERSTELLEN': self.restore_backup() def new_connection_handler(self, connection): # notify newly connected clients about the current state of things # (these will notify *all* clients, but that shouldn't be a problem) self.widget_mgr.send_stack_update(force=True) self.puzzle.notify_state_update("new_connection") def get_screen_width(self): H, W = self.screen.getmaxyx() return W # from https://stackoverflow.com/a/9567831 def memory_dump(self): log.info("collecting garbage...") gc.collect() log.info("dumping memory to 'memory_dump.pickle'...") xs = [] for obj in gc.get_objects(): i = id(obj) size = sys.getsizeof(obj, 0) # referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')] referents = [ id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__') ] cls = None if hasattr(obj, '__class__'): cls = str(obj.__class__) xs.append({ 'id': i, 'class': cls, 'str': str(obj), 'size': size, 'referents': referents }) with open('memory_dump.pickle', 'wb') as dump: pickle.dump(xs, dump, pickle.HIGHEST_PROTOCOL) log.info("memory dump complete") def wakeup_screen(self): log.info("sending key to wakeup screen") subprocess.check_call(["xdotool", "key", "Up"])
class WidgetManager: def __init__(self, app): self.app = app self.screen = app.screen self.widgets = [] # although 1 would be ideal, this will drift and thus we will have some # values twice which does not look good. # TODO: only re-render widgets which need periodic refreshing self.periodic_refresh_timer = WaitableTimer(self.app.sel, 0.25, self.render, True) self.periodic_refresh_timer.start() self.last_update = None def add(self, widget): log.debug("adding new widget: {}".format(widget)) self.widgets.append(widget) self.send_stack_update() def show(self, widget): log.debug("showing widget: {}".format(widget)) if not widget in self.widgets: self.add(widget) self.raise_to_fg(widget) widget.visible = True def remove(self, widget): if widget in self.widgets: log.debug("removing widget: {}".format(widget)) self.widgets.remove(widget) else: log.debug( "not removing widget: {}, is already removed".format(widget)) widget.visible = False self.send_stack_update() def remove_all(self): log.debug("removing all widgets") self.widgets = [] self.send_stack_update() self.render(clear=True) def raise_to_fg(self, widget): # move widget to end of list log.debug("raising widget to foreground: {}".format(widget)) idx = self.widgets.index(widget) self.widgets.append(self.widgets.pop(idx)) self.send_stack_update() def send_stack_update(self, force=False): if not self.widgets: top = None else: top = self.widgets[-1] if not force and self.last_update == top: return # don't send an update when nothing has changed self.last_update = top event = { "event": "window_stack_update", "top_window": { "type": str(type(top).__name__).lower(), } } if isinstance(top, Popup): event['top_window']['title'] = top.title event['top_window']['text'] = top.text self.app.mi.send_packet(event) def handle_input(self, key): if len(self.widgets) > 0: return self.widgets[-1].handle_input(key) else: return False def show_popup(self, title, text, callback=None, buttons=['OK']): return self.show_popup_obj( Popup(self.app, self.screen, title, text, buttons), title, text, callback) def show_input(self, title, text, callback=None, is_password_input=False): p = self.show_popup_obj(InputPopup(self.app, self.screen, title, text), title, text, callback) p.is_password_input = is_password_input return p def show_popup_obj(self, popup, title, text, callback=None): self.add(popup) def wrapped_callback(btn): nonlocal popup # otherwise assignment to this variable will mark it as local popup.visible = False #popup.screen.clear() self.remove(popup) log.info( 'popup wrapped_callback, restoring focus to {}'.format( self.widgets[-1]) ) # this fails if there is NO widget, but that should never happen self.screen.clear() self.screen.refresh() del popup popup = None if callback: callback(btn) self.screen.clear() popup.callback = wrapped_callback return popup def render(self, clear=False): if clear: self.screen.clear() self.screen.refresh() for widget in self.widgets: widget.render() curses.doupdate()