def __init__(self): self.version = get_software_version() logger.info("KlipperScreen version: %s" % self.version) self._config = KlipperScreenConfig() self.printer = Printer({ "software_version": "Unknown" }, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales') _ = self.lang.gettext self.apiclient = KlippyRest("127.0.0.1",7125) Gtk.Window.__init__(self) self.width = self._config.get_main_config().getint("width", Gdk.Screen.get_width(Gdk.Screen.get_default())) self.height = self._config.get_main_config().getint("height", Gdk.Screen.get_height(Gdk.Screen.get_default())) self.set_default_size(self.width, self.height) self.set_resizable(False) logger.info("Screen resolution: %sx%s" % (self.width, self.height)) self.gtk = KlippyGtk(self.width, self.height) self.init_style() #self._load_panels() self.printer_initializing(_("Initializing")) self._ws = KlippyWebsocket(self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }) self._ws.connect() # Disable DPMS os.system("/usr/bin/xset -display :0 s off") os.system("/usr/bin/xset -display :0 -dpms") os.system("/usr/bin/xset -display :0 s noblank") return
def __init__(self): self.version = get_software_version() logger.info("KlipperScreen version: %s" % self.version) parser = argparse.ArgumentParser( description="KlipperScreen - A GUI for Klipper") parser.add_argument( "-c", "--configfile", default="~/KlipperScreen.conf", metavar='<configfile>', help="Location of KlipperScreen configuration file") args = parser.parse_args() configfile = os.path.normpath(os.path.expanduser(args.configfile)) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', fallback=True) self._config = KlipperScreenConfig(configfile, self.lang, self) self.printer = Printer({"software_version": "Unknown"}, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }) self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, "printing": self.state_printing, "ready": self.state_ready, "startup": self.state_startup, "shutdown": self.state_shutdown }) logger.debug("OS Language: %s" % os.getenv('LANG')) self.lang_ltr = True for lang in self.rtl_languages: if os.getenv('LANG').lower().startswith(lang): self.lang_ltr = False Gtk.Widget.set_default_direction(Gtk.TextDirection.RTL) logger.debug("Enabling RTL mode") break _ = self.lang.gettext self.apiclient = KlippyRest( self._config.get_main_config_option("moonraker_host"), self._config.get_main_config_option("moonraker_port"), self._config.get_main_config_option("moonraker_api_key", False)) powerdevs = self.apiclient.send_request("machine/device_power/devices") if powerdevs != False: self.printer.configure_power_devices(powerdevs['result']) Gtk.Window.__init__(self) self.width = self._config.get_main_config().getint( "width", Gdk.Screen.get_width(Gdk.Screen.get_default())) self.height = self._config.get_main_config().getint( "height", Gdk.Screen.get_height(Gdk.Screen.get_default())) self.set_default_size(self.width, self.height) self.set_resizable(False) logger.info("Screen resolution: %sx%s" % (self.width, self.height)) self.gtk = KlippyGtk(self.width, self.height) self.init_style() #self._load_panels() self.printer_initializing(_("Initializing")) self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }, self._config.get_main_config_option("moonraker_host"), self._config.get_main_config_option("moonraker_port")) self._ws.initial_connect() # Disable DPMS os.system("/usr/bin/xset -display :0 -dpms") self.set_screenblanking_timeout( self._config.get_main_config_option('screen_blanking')) # Change cursor to blank self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR)) return
class KlipperScreen(Gtk.Window): """ Class for creating a screen for Klipper via HDMI """ _cur_panels = [] bed_temp_label = None currentPanel = None files = None filename = "" keyboard = None keyboard_height = 200 last_update = {} load_panel = {} number_tools = 1 panels = {} popup_message = None printer = None rtl_languages = ['he_il'] subscriptions = [] shutdown = True def __init__(self): self.version = get_software_version() logger.info("KlipperScreen version: %s" % self.version) parser = argparse.ArgumentParser( description="KlipperScreen - A GUI for Klipper") parser.add_argument( "-c", "--configfile", default="~/KlipperScreen.conf", metavar='<configfile>', help="Location of KlipperScreen configuration file") args = parser.parse_args() configfile = os.path.normpath(os.path.expanduser(args.configfile)) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', fallback=True) self._config = KlipperScreenConfig(configfile, self.lang, self) self.printer = Printer({"software_version": "Unknown"}, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }) self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, "printing": self.state_printing, "ready": self.state_ready, "startup": self.state_startup, "shutdown": self.state_shutdown }) logger.debug("OS Language: %s" % os.getenv('LANG')) self.lang_ltr = True for lang in self.rtl_languages: if os.getenv('LANG').lower().startswith(lang): self.lang_ltr = False Gtk.Widget.set_default_direction(Gtk.TextDirection.RTL) logger.debug("Enabling RTL mode") break _ = self.lang.gettext self.apiclient = KlippyRest( self._config.get_main_config_option("moonraker_host"), self._config.get_main_config_option("moonraker_port"), self._config.get_main_config_option("moonraker_api_key", False)) powerdevs = self.apiclient.send_request("machine/device_power/devices") if powerdevs != False: self.printer.configure_power_devices(powerdevs['result']) Gtk.Window.__init__(self) self.width = self._config.get_main_config().getint( "width", Gdk.Screen.get_width(Gdk.Screen.get_default())) self.height = self._config.get_main_config().getint( "height", Gdk.Screen.get_height(Gdk.Screen.get_default())) self.set_default_size(self.width, self.height) self.set_resizable(False) logger.info("Screen resolution: %sx%s" % (self.width, self.height)) self.gtk = KlippyGtk(self.width, self.height) self.init_style() #self._load_panels() self.printer_initializing(_("Initializing")) self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }, self._config.get_main_config_option("moonraker_host"), self._config.get_main_config_option("moonraker_port")) self._ws.initial_connect() # Disable DPMS os.system("/usr/bin/xset -display :0 -dpms") self.set_screenblanking_timeout( self._config.get_main_config_option('screen_blanking')) # Change cursor to blank self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR)) return def ws_subscribe(self): requested_updates = { "objects": { "bed_mesh": ["profile_name", "mesh_max", "mesh_min", "probed_matrix"], "configfile": ["config"], "fan": ["speed"], "gcode_move": [ "extrude_factor", "gcode_position", "homing_origin", "speed_factor" ], "heater_bed": ["target", "temperature"], "idle_timeout": ["state"], "pause_resume": ["is_paused"], "print_stats": [ "print_duration", "total_duration", "filament_used", "filename", "state", "message" ], "toolhead": [ "homed_axes", "estimated_print_time", "print_time", "position", "extruder" ], "virtual_sdcard": ["file_position", "is_active", "progress"], "webhooks": ["state", "state_message"] } } for extruder in self.printer.get_tools(): requested_updates['objects'][extruder] = [ "target", "temperature", "pressure_advance", "smooth_time" ] self._ws.klippy.object_subscription(requested_updates) def _load_panel(self, panel, *args): if not panel in self.load_panel: logger.debug("Loading panel: %s" % panel) panel_path = os.path.join(os.path.dirname(__file__), 'panels', "%s.py" % panel) logger.info("Panel path: %s" % panel_path) if not os.path.exists(panel_path): msg = f"Panel {panel} does not exist" logger.info(msg) raise Exception(msg) module = importlib.import_module("panels.%s" % panel) if not hasattr(module, "create_panel"): msg = f"Cannot locate create_panel function for {panel}" logger.info(msg) raise Exception(msg) self.load_panel[panel] = getattr(module, "create_panel") try: return self.load_panel[panel](*args) except Exception: msg = f"Unable to create panel {panel}" logger.exception(msg) raise Exception(msg) def show_panel(self, panel_name, type, title, remove=None, pop=True, **kwargs): if panel_name not in self.panels: self.panels[panel_name] = self._load_panel(type, self, title) try: if kwargs != {}: self.panels[panel_name].initialize(panel_name, **kwargs) else: self.panels[panel_name].initialize(panel_name) except: del self.panels[panel_name] logger.exception("Unable to load panel %s" % type) self.show_error_modal("Unable to load panel %s" % type) return if hasattr(self.panels[panel_name], "process_update"): self.panels[panel_name].process_update("notify_status_update", self.printer.get_data()) if remove == 2: self._remove_all_panels() elif remove == 1: self._remove_current_panel(pop) self.add(self.panels[panel_name].get()) self.show_all() if hasattr(self.panels[panel_name], "activate"): self.panels[panel_name].activate() self.show_all() self._cur_panels.append(panel_name) logger.debug("Current panel hierarchy: %s", str(self._cur_panels)) def show_popup_message(self, message): if self.popup_message != None: self.close_popup_message() box = Gtk.Box() box.get_style_context().add_class("message_popup") box.set_size_request(self.width, self.gtk.get_header_size()) label = Gtk.Label() if "must home axis first" in message.lower(): message = "Must home all axis first." label.set_text(message) close = Gtk.Button.new_with_label("X") close.set_can_focus(False) close.props.relief = Gtk.ReliefStyle.NONE close.connect("clicked", self.close_popup_message) box.pack_start(label, True, True, 0) box.pack_end(close, False, False, 0) box.set_halign(Gtk.Align.CENTER) cur_panel = self.panels[self._cur_panels[-1]] for i in ['back', 'estop', 'home']: if i in cur_panel.control: cur_panel.control[i].set_sensitive(False) cur_panel.get().put(box, 0, 0) self.show_all() self.popup_message = box GLib.timeout_add(10000, self.close_popup_message) return False def close_popup_message(self, widget=None): if self.popup_message == None: return cur_panel = self.panels[self._cur_panels[-1]] for i in ['back', 'estop', 'home']: if i in cur_panel.control: cur_panel.control[i].set_sensitive(True) cur_panel.get().remove(self.popup_message) self.popup_message = None self.show_all() def show_error_modal(self, err): _ = self.lang.gettext logger.exception("Showing error modal: %s", err) buttons = [{"name": _("Go Back"), "response": Gtk.ResponseType.CANCEL}] label = Gtk.Label() label.set_markup(("%s \n\n" % err) + _( "Check /tmp/KlipperScreen.log for more information.\nPlease submit an issue on GitHub for help." )) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) dialog = self.gtk.Dialog(self, buttons, label, self.error_modal_response) def error_modal_response(self, widget, response_id): widget.destroy() def init_style(self): style_provider = Gtk.CssProvider() #style_provider.load_from_path(klipperscreendir + "/style.css") css = open(klipperscreendir + "/styles/style.css") css_data = css.read() css.close() css_data = css_data.replace("KS_FONT_SIZE", str(self.gtk.get_font_size())) style_provider = Gtk.CssProvider() style_provider.load_from_data(css_data.encode()) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def is_printing(self): return "job_status" in self._cur_panels def _go_to_submenu(self, widget, name): logger.info("#### Go to submenu " + str(name)) #self._remove_current_panel(False) # Find current menu item panels = list(self._cur_panels) if "main_panel" in self._cur_panels: menu = "__main" elif "splash_screen" in self._cur_panels: menu = "__splashscreen" else: menu = "__print" logger.info("#### Menu " + str(menu)) disname = self._config.get_menu_name(menu, name) menuitems = self._config.get_menu_items(menu, name) if len(menuitems) == 0: logger.info("No items in menu, returning.") return self.show_panel(self._cur_panels[-1] + '_' + name, "menu", disname, 1, False, display_name=disname, items=menuitems) def _remove_all_panels(self): while len(self._cur_panels) > 0: self._remove_current_panel(True, False) self.show_all() def _remove_current_panel(self, pop=True, show=True): if len(self._cur_panels) > 0: self.remove(self.panels[self._cur_panels[-1]].get()) if pop == True: self._cur_panels.pop() if len(self._cur_panels) > 0: self.add(self.panels[self._cur_panels[-1]].get()) if show == True: self.show_all() def _menu_go_back(self, widget=None): logger.info("#### Menu go back") self.remove_keyboard() self._remove_current_panel() def _menu_go_home(self): logger.info("#### Menu go home") self.remove_keyboard() while len(self._cur_panels) > 1: self._remove_current_panel() def add_subscription(self, panel_name): add = True for sub in self.subscriptions: if sub == panel_name: return self.subscriptions.append(panel_name) def remove_subscription(self, panel_name): for i in range(len(self.subscriptions)): if self.subscriptions[i] == panel_name: self.subscriptions.pop(i) return def set_screenblanking_timeout(self, time): logger.debug("Changing screenblanking to: %s" % time) if time == "off": os.system("/usr/bin/xset -display :0 s off") os.system("/usr/bin/xset -display :0 s noblank") return time = int(time) if time < 0: return os.system("/usr/bin/xset -display :0 s on") os.system("/usr/bin/xset -display :0 s %s" % time) def state_disconnected(self): _ = self.lang.gettext logger.debug("### Going to disconnected") self.printer_initializing(_("Klipper has disconnected")) def state_error(self): _ = self.lang.gettext msg = self.printer.get_stat("webhooks", "state_message") if "FIRMWARE_RESTART" in msg: self.printer_initializing( _("Klipper has encountered an error.\nIssue a FIRMWARE_RESTART to attempt fixing the issue." )) elif "micro-controller" in msg: self.printer_initializing( _("Klipper has encountered an error with the micro-controller.\nPlease recompile and flash." )) else: self.printer_initializing(_("Klipper has encountered an error.")) def state_printing(self): if "job_status" in self._cur_panels: return self.printer_printing() def state_ready(self): # Do not return to main menu if completing a job, timeouts/user input will return if "job_status" in self._cur_panels or "main_menu" in self._cur_panels: return self.printer_ready() def state_startup(self): _ = self.lang.gettext self.printer_initializing(_("Klipper is attempting to start")) def state_shutdown(self): _ = self.lang.gettext self.printer_initializing(_("Klipper has shutdown")) def _websocket_callback(self, action, data): _ = self.lang.gettext if action == "notify_klippy_disconnected": logger.debug("Received notify_klippy_disconnected") self.printer.change_state("disconnected") return elif action == "notify_klippy_ready": self.printer.change_state("ready") elif action == "notify_status_update" and self.printer.get_state( ) != "shutdown": self.printer.process_update(data) elif action == "notify_filelist_changed": logger.debug("Filelist changed: %s", json.dumps(data, indent=2)) #self.files.add_file() elif action == "notify_metadata_update": self.files.request_metadata(data['filename']) elif action == "notify_power_changed": logger.debug("Power status changed: %s", data) self.printer.process_power_update(data) elif self.printer.get_state() not in [ "error", "shutdown" ] and action == "notify_gcode_response": if "Klipper state: Shutdown" in data: logger.debug( "Shutdown in gcode response, changing state to shutdown") self.printer.change_state("shutdown") if not (data.startswith("B:") and re.search( r'B:[0-9\.]+\s/[0-9\.]+\sT[0-9]+:[0-9\.]+', data)): if data.startswith("!! "): self.show_popup_message(data[3:]) logger.debug(json.dumps([action, data], indent=2)) for sub in self.subscriptions: self.panels[sub].process_update(action, data) def _confirm_send_action(self, widget, text, method, params={}): _ = self.lang.gettext buttons = [{ "name": _("Continue"), "response": Gtk.ResponseType.OK }, { "name": _("Cancel"), "response": Gtk.ResponseType.CANCEL }] try: env = Environment(extensions=["jinja2.ext.i18n"]) env.install_gettext_translations(self.lang) j2_temp = env.from_string(text) text = j2_temp.render() except: logger.debug("Error parsing jinja for confirm_send_action") label = Gtk.Label() label.set_markup(text) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) dialog = self.gtk.Dialog(self, buttons, label, self._confirm_send_action_response, method, params) def _confirm_send_action_response(self, widget, response_id, method, params): if response_id == Gtk.ResponseType.OK: self._send_action(widget, method, params) widget.destroy() def _send_action(self, widget, method, params): self._ws.send_method(method, params) def printer_initializing(self, text=None): self.shutdown = True self.close_popup_message() self.show_panel('splash_screen', "splash_screen", "Splash Screen", 2) if text != None: self.panels['splash_screen'].update_text(text) self.panels['splash_screen'].show_restart_buttons() def init_printer(self): _ = self.lang.gettext status_objects = [ 'bed_mesh', 'idle_timeout', 'configfile', 'gcode_move', 'fan', 'toolhead', 'virtual_sdcard', 'print_stats', 'heater_bed', 'extruder', 'pause_resume', 'webhooks' ] printer_info = self.apiclient.get_printer_info() logger.debug("Sending request %s" % "printer/objects/query?" + "&".join(status_objects)) data = self.apiclient.send_request("printer/objects/query?" + "&".join(status_objects)) powerdevs = self.apiclient.send_request("machine/device_power/devices") data = data['result']['status'] if self.files == None: self.files = KlippyFiles(self) else: self.files.add_timeout() # Reinitialize printer, in case the printer was shut down and anything has changed. self.printer.reinit(printer_info['result'], data) self.ws_subscribe() if powerdevs != False: self.printer.configure_power_devices(powerdevs['result']) def printer_ready(self): self.close_popup_message() # Force update to printer webhooks state in case the update is missed due to websocket subscribe not yet sent self.printer.process_update({ "webhooks": { "state": "ready", "state_message": "Printer is ready" } }) self.show_panel('main_panel', "main_menu", "Main Menu", 2, items=self._config.get_menu_items("__main"), extrudercount=self.printer.get_extruder_count()) self.ws_subscribe() if "job_status" in self.panels: self.remove_subscription("job_status") del self.panels["job_status"] def printer_printing(self): self.files.remove_timeout() self.close_popup_message() self.show_panel('job_status', "job_status", "Print Status", 2) def show_keyboard(self, widget=None): if self.keyboard is not None: return env = os.environ.copy() env["MB_KBD_CONFIG"] = "/home/pi/.matchbox/keyboard.xml" env["MB_KBD_CONFIG"] = "ks_includes/locales/keyboard.xml" p = subprocess.Popen(["matchbox-keyboard", "--xid"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) #p = subprocess.Popen(["onboard", "--xid"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) xid = int(p.stdout.readline()) logger.debug("XID %s" % xid) logger.debug("PID %s" % p.pid) keyboard = Gtk.Socket() #keyboard.connect("plug-added", self.plug_added) box = Gtk.VBox() box.set_size_request(self.width, self.keyboard_height) box.add(keyboard) cur_panel = self.panels[self._cur_panels[-1]] #for i in ['back','estop','home']: # if i in cur_panel.control: # cur_panel.control[i].set_sensitive(False) cur_panel.get().put(box, 0, self.height - 200) self.show_all() keyboard.add_id(xid) keyboard.show() self.keyboard = { "box": box, "panel": cur_panel.get(), "process": p, "socket": keyboard } def remove_keyboard(self, widget=None): if self.keyboard is None: return self.keyboard['panel'].remove(self.keyboard['box']) os.kill(self.keyboard['process'].pid, signal.SIGTERM) self.keyboard = None
class KlipperScreen(Gtk.Window): """ Class for creating a screen for Klipper via HDMI """ _cur_panels = [] bed_temp_label = None connecting = False connected_printer = None currentPanel = None files = None filename = "" keyboard = None keyboard_height = 200 last_update = {} load_panel = {} number_tools = 1 panels = {} popup_message = None printer = None printer_select_callbacks = [] printer_select_prepanel = None rtl_languages = ['he_il'] subscriptions = [] shutdown = True updating = False update_queue = [] _ws = None def __init__(self, args, version): self.dpms_timeout = None self.version = version configfile = os.path.normpath(os.path.expanduser(args.configfile)) self._config = KlipperScreenConfig(configfile, self) self.lang = self._config.get_lang() self.network_interfaces = netifaces.interfaces() self.wireless_interfaces = [ int for int in self.network_interfaces if int.startswith('w') ] self.wifi = None if len(self.wireless_interfaces) > 0: logging.info("Found wireless interfaces: %s" % self.wireless_interfaces) self.wifi = WifiManager(self.wireless_interfaces[0]) logging.debug("OS Language: %s" % os.getenv('LANG')) self.lang_ltr = True for lang in self.rtl_languages: if os.getenv('LANG').lower().startswith(lang): self.lang_ltr = False Gtk.Widget.set_default_direction(Gtk.TextDirection.RTL) logging.debug("Enabling RTL mode") break _ = self.lang.gettext Gtk.Window.__init__(self) self.width = self._config.get_main_config().getint( "width", Gdk.Screen.get_width(Gdk.Screen.get_default())) self.height = self._config.get_main_config().getint( "height", Gdk.Screen.get_height(Gdk.Screen.get_default())) self.set_default_size(self.width, self.height) self.set_resizable(False) logging.info("Screen resolution: %sx%s" % (self.width, self.height)) self.theme = self._config.get_main_config_option('theme') self.gtk = KlippyGtk(self, self.width, self.height, self.theme) self.keyboard_height = self.gtk.get_keyboard_height() self.init_style() self.base_panel = BasePanel(self, "Base Panel", False) self.add(self.base_panel.get()) self.show_all() self.base_panel.activate() self.printer_initializing(_("Initializing")) self.set_screenblanking_timeout( self._config.get_main_config_option('screen_blanking')) # Move mouse to 0,0 os.system("/usr/bin/xdotool mousemove 0 0") # Change cursor to blank if self._config.get_main_config().getboolean("show_cursor", fallback=False): self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) else: self.get_window().set_cursor( Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR)) printers = self._config.get_printers() logging.debug("Printers: %s" % printers) if len(printers) == 1: pname = list(self._config.get_printers()[0])[0] self.connect_printer(pname) else: self.show_panel("printer_select", "printer_select", "Printer Select", 2) def connect_printer_widget(self, widget, name): self.connect_printer(name) def connect_printer(self, name): _ = self.lang.gettext if self.connected_printer == name: if self.printer_select_prepanel != None: self.show_panel(self.printer_select_prepanel, "", "", 2) self.printer_select_prepanel = None while len(self.printer_select_callbacks) > 0: i = self.printer_select_callbacks.pop(0) i() return self.printer_select_callbacks = [] self.printer_select_prepanel = None if self.files is not None: self.files = None for printer in self._config.get_printers(): pname = list(printer)[0] if pname != name: continue data = printer[pname] break if self._ws is not None: self._ws.close() self.connecting = True logging.info("Connecting to printer: %s" % name) self.apiclient = KlippyRest(data["moonraker_host"], data["moonraker_port"], data["moonraker_api_key"]) self.printer = Printer({"software_version": "Unknown"}, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }, self.state_execute) self._remove_all_panels() panels = list(self.panels) if len(self.subscriptions) > 0: self.subscriptions = [] for panel in panels: del self.panels[panel] self.printer_initializing(_("Connecting to %s") % name) self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, "paused": self.state_paused, "printing": self.state_printing, "ready": self.state_ready, "startup": self.state_startup, "shutdown": self.state_shutdown }) powerdevs = self.apiclient.send_request("machine/device_power/devices") logging.debug("Found power devices: %s" % powerdevs) if powerdevs != False: self.printer.configure_power_devices(powerdevs['result']) self.panels['splash_screen'].show_restart_buttons() self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }, data["moonraker_host"], data["moonraker_port"]) self.files = KlippyFiles(self) self._ws.initial_connect() self.connecting = False self.connected_printer = name logging.debug("Connected to printer: %s" % name) def ws_subscribe(self): requested_updates = { "objects": { "bed_mesh": ["profile_name", "mesh_max", "mesh_min", "probed_matrix"], "configfile": ["config"], "display_status": ["progress", "message"], "fan": ["speed"], "gcode_move": [ "extrude_factor", "gcode_position", "homing_origin", "speed_factor" ], "idle_timeout": ["state"], "pause_resume": ["is_paused"], "print_stats": [ "print_duration", "total_duration", "filament_used", "filename", "state", "message" ], "toolhead": [ "homed_axes", "estimated_print_time", "print_time", "position", "extruder" ], "virtual_sdcard": ["file_position", "is_active", "progress"], "webhooks": ["state", "state_message"] } } for extruder in self.printer.get_tools(): requested_updates['objects'][extruder] = [ "target", "temperature", "pressure_advance", "smooth_time" ] for h in self.printer.get_heaters(): requested_updates['objects'][h] = ["target", "temperature"] self._ws.klippy.object_subscription(requested_updates) def _load_panel(self, panel, *args): if not panel in self.load_panel: logging.debug("Loading panel: %s" % panel) panel_path = os.path.join(os.path.dirname(__file__), 'panels', "%s.py" % panel) logging.info("Panel path: %s" % panel_path) if not os.path.exists(panel_path): msg = f"Panel {panel} does not exist" logging.info(msg) raise Exception(msg) module = importlib.import_module("panels.%s" % panel) if not hasattr(module, "create_panel"): msg = f"Cannot locate create_panel function for {panel}" logging.info(msg) raise Exception(msg) self.load_panel[panel] = getattr(module, "create_panel") try: return self.load_panel[panel](*args) except Exception: msg = f"Unable to create panel {panel}" logging.exception(msg) raise Exception(msg) def show_panel(self, panel_name, type, title, remove=None, pop=True, **kwargs): if panel_name not in self.panels: try: self.panels[panel_name] = self._load_panel(type, self, title) if kwargs != {}: self.panels[panel_name].initialize(panel_name, **kwargs) else: self.panels[panel_name].initialize(panel_name) except: if panel_name in self.panels: del self.panels[panel_name] logging.exception("Unable to load panel %s" % type) self.show_error_modal("Unable to load panel %s" % type) return if hasattr(self.panels[panel_name], "process_update"): self.panels[panel_name].process_update("notify_status_update", self.printer.get_data()) try: if remove == 2: self._remove_all_panels() elif remove == 1: self._remove_current_panel(pop) logging.debug("Attaching panel %s" % panel_name) self.base_panel.add_content(self.panels[panel_name]) logging.debug("Showing back. count: %s" % len(self._cur_panels)) if len(self._cur_panels) == 0: self.base_panel.show_back(False) else: self.base_panel.show_back(True) self.show_all() if hasattr(self.panels[panel_name], "process_update"): self.panels[panel_name].process_update( "notify_status_update", self.printer.get_updates()) if hasattr(self.panels[panel_name], "activate"): self.panels[panel_name].activate() self.show_all() except: logging.exception("Error attaching panel") self._cur_panels.append(panel_name) logging.debug("Current panel hierarchy: %s", str(self._cur_panels)) def show_popup_message(self, message): if self.popup_message != None: self.close_popup_message() box = Gtk.Box() box.get_style_context().add_class("message_popup") box.set_size_request(self.width, self.gtk.get_header_size()) label = Gtk.Label() if "must home axis first" in message.lower(): message = "Must home all axis first." label.set_text(message) close = Gtk.Button.new_with_label("X") close.set_can_focus(False) close.props.relief = Gtk.ReliefStyle.NONE close.connect("clicked", self.close_popup_message) box.pack_start(label, True, True, 0) box.pack_end(close, False, False, 0) box.set_halign(Gtk.Align.CENTER) cur_panel = self.panels[self._cur_panels[-1]] self.base_panel.get().put(box, 0, 0) self.show_all() self.popup_message = box GLib.timeout_add(10000, self.close_popup_message) return False def close_popup_message(self, widget=None): if self.popup_message == None: return self.base_panel.get().remove(self.popup_message) self.popup_message = None self.show_all() def show_error_modal(self, err): _ = self.lang.gettext logging.exception("Showing error modal: %s", err) buttons = [{"name": _("Go Back"), "response": Gtk.ResponseType.CANCEL}] label = Gtk.Label() label.set_markup(("%s \n\n" % err) + _( "Check /tmp/KlipperScreen.log for more information.\nPlease submit an issue on GitHub for help." )) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_vexpand(True) label.set_valign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) dialog = self.gtk.Dialog(self, buttons, label, self.error_modal_response) def error_modal_response(self, widget, response_id): widget.destroy() def restart_warning(self, value): _ = self.lang.gettext logging.debug("Showing restart warning because: %s" % value) buttons = [{ "name": _("Cancel"), "response": Gtk.ResponseType.CANCEL }, { "name": _("Restart"), "response": Gtk.ResponseType.OK }] label = Gtk.Label() label.set_markup( _("To apply %s KlipperScreen needs to be restarted" % value)) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_vexpand(True) label.set_valign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) dialog = self.gtk.Dialog(self, buttons, label, self.restart_ks) def restart_ks(self, widget, response_id): if response_id == Gtk.ResponseType.OK: logging.debug("Restarting") os.system("sudo systemctl restart KlipperScreen") widget.destroy() def init_style(self): style_provider = Gtk.CssProvider() css = open(klipperscreendir + "/styles/%s/style.css" % (self.theme)) css_data = css.read() css.close() css_data = css_data.replace("KS_FONT_SIZE", str(self.gtk.get_font_size())) style_provider = Gtk.CssProvider() style_provider.load_from_data(css_data.encode()) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def is_keyboard_showing(self): if self.keyboard == None: return False return True def is_printing(self): return self.printer.get_state() == "printing" def is_updating(self): return self.updating def _go_to_submenu(self, widget, name): logging.info("#### Go to submenu " + str(name)) #self._remove_current_panel(False) # Find current menu item panels = list(self._cur_panels) if "main_panel" in self._cur_panels: menu = "__main" elif "splash_screen" in self._cur_panels: menu = "__splashscreen" else: menu = "__print" logging.info("#### Menu " + str(menu)) disname = self._config.get_menu_name(menu, name) menuitems = self._config.get_menu_items(menu, name) if len(menuitems) == 0: logging.info("No items in menu, returning.") return self.show_panel(self._cur_panels[-1] + '_' + name, "menu", disname, 1, False, display_name=disname, items=menuitems) def _remove_all_panels(self): while len(self._cur_panels) > 0: self._remove_current_panel(True, False) self.show_all() def _remove_current_panel(self, pop=True, show=True): if len(self._cur_panels) > 0: self.base_panel.remove( self.panels[self._cur_panels[-1]].get_content()) if pop == True: self._cur_panels.pop() if len(self._cur_panels) > 0: self.base_panel.add_content( self.panels[self._cur_panels[-1]]) self.base_panel.show_back(False if len(self._cur_panels) == 1 else True) if hasattr(self.panels[self._cur_panels[-1]], "process_update"): self.panels[self._cur_panels[-1]].process_update( "notify_status_update", self.printer.get_updates()) if show == True: self.show_all() def _menu_go_back(self, widget=None): logging.info("#### Menu go back") self.remove_keyboard() self.close_popup_message() self._remove_current_panel() def _menu_go_home(self): logging.info("#### Menu go home") self.remove_keyboard() self.close_popup_message() while len(self._cur_panels) > 1: self._remove_current_panel() def add_subscription(self, panel_name): add = True for sub in self.subscriptions: if sub == panel_name: return self.subscriptions.append(panel_name) def remove_subscription(self, panel_name): for i in range(len(self.subscriptions)): if self.subscriptions[i] == panel_name: self.subscriptions.pop(i) return def check_dpms_state(self): state = functions.get_DPMS_state() if state == functions.DPMS_State.Off and "screensaver" not in self._cur_panels: logging.info("### Creating screensaver panel") self.show_panel("screensaver", "screensaver", "Screen Saver", 1, False) elif state == functions.DPMS_State.On and "screensaver" in self._cur_panels: logging.info("### Remove screensaver panel") self._menu_go_back() return True def wake_screen(self): self.time = self._config.get_main_config_option('screen_blanking') # Wake the screen (it will go to standby as configured) if self.time != "off": logging.debug("Screen wake up") os.system("xset -display :0 dpms force on") def set_screenblanking_timeout(self, time): # Disable screen blanking os.system("xset -display :0 s off") os.system("xset -display :0 s noblank") if functions.dpms_loaded == False: logging.info( "DPMS functions not loaded. Unable to protect on button click when DPMS is enabled." ) logging.debug("Changing power save to: %s" % time) if time == "off": if self.dpms_timeout != None: GLib.source_remove(self.dpms_timeout) self.dpms_timeout = None os.system("xset -display :0 -dpms") return time = int(time) if time < 0: return os.system("xset -display :0 dpms 0 %s 0" % time) if self.dpms_timeout == None and functions.dpms_loaded == True: self.dpms_timeout = GLib.timeout_add(1000, self.check_dpms_state) def set_updating(self, updating=False): if self.updating == True and updating == False: if len(self.update_queue) > 0: i = self.update_queue.pop() self.update_queue = [] i[0](i[1]) self.updating = updating def show_printer_select(self, widget=None): logging.debug("Saving panel: %s" % self._cur_panels[0]) self.printer_select_prepanel = self._cur_panels[0] self.show_panel("printer_select", "printer_select", "Printer Select", 2) def state_execute(self, callback, prev_state): if self.is_updating(): self.update_queue.append([callback, prev_state]) else: callback(prev_state) def state_disconnected(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_disconnected] return _ = self.lang.gettext logging.debug("### Going to disconnected") self.printer_initializing(_("Klipper has disconnected")) for panel in list(self.panels): if panel in ["printer_select", "splash_screen"]: continue #del self.panels[panel] def state_error(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_error] return _ = self.lang.gettext msg = self.printer.get_stat("webhooks", "state_message") if "FIRMWARE_RESTART" in msg: self.printer_initializing( _("Klipper has encountered an error.\nIssue a FIRMWARE_RESTART to attempt fixing the issue." )) elif "micro-controller" in msg: self.printer_initializing( _("Klipper has encountered an error with the micro-controller.\nPlease recompile and flash." )) else: self.printer_initializing(_("Klipper has encountered an error.")) for panel in list(self.panels): if panel in ["printer_select", "splash_screen"]: continue del self.panels[panel] def state_paused(self, prev_state): if "job_status" not in self._cur_panels: self.printer_printing() def state_printing(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_printing] return if "job_status" not in self._cur_panels: self.printer_printing() else: self.panels["job_status"].new_print() def state_ready(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_ready] return # Do not return to main menu if completing a job, timeouts/user input will return if "job_status" in self._cur_panels or "main_menu" in self._cur_panels: return if prev_state not in ['paused', 'printing']: self.init_printer() self.base_panel._printer = self.printer self.base_panel.show_heaters() self.printer_ready() def state_startup(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_startup] return _ = self.lang.gettext self.printer_initializing(_("Klipper is attempting to start")) def state_shutdown(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_shutdown] return _ = self.lang.gettext self.printer_initializing(_("Klipper has shutdown")) def _websocket_callback(self, action, data): _ = self.lang.gettext if self.connecting == True: return if action == "notify_klippy_disconnected": logging.debug("Received notify_klippy_disconnected") self.printer.change_state("disconnected") return elif action == "notify_klippy_ready": self.printer.change_state("ready") elif action == "notify_status_update" and self.printer.get_state( ) != "shutdown": self.printer.process_update(data) elif action == "notify_filelist_changed": logging.debug("Filelist changed: %s", json.dumps(data, indent=2)) if self.files != None: self.files.process_update(data) elif action == "notify_metadata_update": self.files.request_metadata(data['filename']) elif action == "notify_update_response": logging.info("%s: %s" % (action, data)) elif action == "notify_power_changed": logging.debug("Power status changed: %s", data) self.printer.process_power_update(data) elif self.printer.get_state() not in [ "error", "shutdown" ] and action == "notify_gcode_response": if "Klipper state: Shutdown" in data: logging.debug( "Shutdown in gcode response, changing state to shutdown") self.printer.change_state("shutdown") if not (data.startswith("B:") and re.search( r'B:[0-9\.]+\s/[0-9\.]+\sT[0-9]+:[0-9\.]+', data)): if data.startswith("!! "): self.show_popup_message(data[3:]) #logging.debug(json.dumps([action, data], indent=2)) self.base_panel.process_update(action, data) if self._cur_panels[-1] in self.subscriptions: self.panels[self._cur_panels[-1]].process_update(action, data) def _confirm_send_action(self, widget, text, method, params={}): _ = self.lang.gettext buttons = [{ "name": _("Continue"), "response": Gtk.ResponseType.OK }, { "name": _("Cancel"), "response": Gtk.ResponseType.CANCEL }] try: env = Environment(extensions=["jinja2.ext.i18n"]) env.install_gettext_translations(self.lang) j2_temp = env.from_string(text) text = j2_temp.render() except: logging.debug("Error parsing jinja for confirm_send_action") label = Gtk.Label() label.set_markup(text) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_vexpand(True) label.set_valign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) dialog = self.gtk.Dialog(self, buttons, label, self._confirm_send_action_response, method, params) def _confirm_send_action_response(self, widget, response_id, method, params): if response_id == Gtk.ResponseType.OK: self._send_action(widget, method, params) widget.destroy() def _send_action(self, widget, method, params): self._ws.send_method(method, params) def printer_initializing(self, text=None): self.shutdown = True self.close_popup_message() self.show_panel('splash_screen', "splash_screen", "Splash Screen", 2) if text != None: self.panels['splash_screen'].update_text(text) self.panels['splash_screen'].show_restart_buttons() def init_printer(self): _ = self.lang.gettext printer_info = self.apiclient.get_printer_info() if printer_info == False: logging.info("Unable to get printer info from moonraker") return False data = self.apiclient.send_request( "printer/objects/query?" + "&".join(PRINTER_BASE_STATUS_OBJECTS)) if data == False: logging.info("Error getting printer object data") return False powerdevs = self.apiclient.send_request("machine/device_power/devices") data = data['result']['status'] # Reinitialize printer, in case the printer was shut down and anything has changed. self.printer.reinit(printer_info['result'], data) self.ws_subscribe() self.files.initialize() self.files.refresh_files() if powerdevs != False: self.printer.configure_power_devices(powerdevs['result']) def printer_ready(self): _ = self.lang.gettext self.close_popup_message() # Force update to printer webhooks state in case the update is missed due to websocket subscribe not yet sent self.printer.process_update({ "webhooks": { "state": "ready", "state_message": "Printer is ready" } }) self.show_panel('main_panel', "main_menu", _("Home"), 2, items=self._config.get_menu_items("__main"), extrudercount=self.printer.get_extruder_count()) self.ws_subscribe() if "job_status" in self.panels: self.remove_subscription("job_status") del self.panels["job_status"] def printer_printing(self): self.close_popup_message() self.show_panel('job_status', "job_status", "Print Status", 2) def show_keyboard(self, widget=None): if self.keyboard is not None: return env = os.environ.copy() env["MB_KBD_CONFIG"] = "/home/pi/.matchbox/keyboard.xml" env["MB_KBD_CONFIG"] = "ks_includes/locales/keyboard.xml" p = subprocess.Popen(["matchbox-keyboard", "--xid"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) xid = int(p.stdout.readline()) logging.debug("XID %s" % xid) logging.debug("PID %s" % p.pid) keyboard = Gtk.Socket() action_bar_width = self.gtk.get_action_bar_width() box = Gtk.VBox() box.set_vexpand(False) box.set_size_request(self.width - action_bar_width, self.keyboard_height) box.add(keyboard) self.base_panel.get_content().pack_end(box, False, 0, 0) self.show_all() keyboard.add_id(xid) keyboard.show() self.keyboard = { "box": box, #"panel": cur_panel.get(), "process": p, "socket": keyboard } def remove_keyboard(self, widget=None): if self.keyboard is None: return self.base_panel.get_content().remove(self.keyboard['box']) os.kill(self.keyboard['process'].pid, signal.SIGTERM) self.keyboard = None
def connect_printer(self, name): _ = self.lang.gettext if self.connected_printer == name: if self.printer_select_prepanel != None: self.show_panel(self.printer_select_prepanel, "", "", 2) self.printer_select_prepanel = None while len(self.printer_select_callbacks) > 0: i = self.printer_select_callbacks.pop(0) i() return self.printer_select_callbacks = [] self.printer_select_prepanel = None if self.files is not None: self.files = None for printer in self._config.get_printers(): pname = list(printer)[0] if pname != name: continue data = printer[pname] break if self._ws is not None: self._ws.close() self.connecting = True logging.info("Connecting to printer: %s" % name) self.apiclient = KlippyRest(data["moonraker_host"], data["moonraker_port"], data["moonraker_api_key"]) self.printer = Printer({"software_version": "Unknown"}, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }, self.state_execute) self._remove_all_panels() panels = list(self.panels) if len(self.subscriptions) > 0: self.subscriptions = [] for panel in panels: del self.panels[panel] self.printer_initializing(_("Connecting to %s") % name) self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, "paused": self.state_paused, "printing": self.state_printing, "ready": self.state_ready, "startup": self.state_startup, "shutdown": self.state_shutdown }) powerdevs = self.apiclient.send_request("machine/device_power/devices") logging.debug("Found power devices: %s" % powerdevs) if powerdevs != False: self.printer.configure_power_devices(powerdevs['result']) self.panels['splash_screen'].show_restart_buttons() self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }, data["moonraker_host"], data["moonraker_port"]) self.files = KlippyFiles(self) self._ws.initial_connect() self.connecting = False self.connected_printer = name logging.debug("Connected to printer: %s" % name)
class KlipperScreen(Gtk.Window): """ Class for creating a screen for Klipper via HDMI """ _cur_panels = [] bed_temp_label = None currentPanel = None files = None filename = "" last_update = {} load_panel = {} number_tools = 1 panels = {} popup_message = None printer = None subscriptions = [] shutdown = True def __init__(self): self.version = get_software_version() logger.info("KlipperScreen version: %s" % self.version) self._config = KlipperScreenConfig() self.printer = Printer({ "software_version": "Unknown" }, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales') _ = self.lang.gettext self.apiclient = KlippyRest("127.0.0.1",7125) Gtk.Window.__init__(self) self.width = self._config.get_main_config().getint("width", Gdk.Screen.get_width(Gdk.Screen.get_default())) self.height = self._config.get_main_config().getint("height", Gdk.Screen.get_height(Gdk.Screen.get_default())) self.set_default_size(self.width, self.height) self.set_resizable(False) logger.info("Screen resolution: %sx%s" % (self.width, self.height)) self.gtk = KlippyGtk(self.width, self.height) self.init_style() #self._load_panels() self.printer_initializing(_("Initializing")) self._ws = KlippyWebsocket(self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }) self._ws.connect() # Disable DPMS os.system("/usr/bin/xset -display :0 s off") os.system("/usr/bin/xset -display :0 -dpms") os.system("/usr/bin/xset -display :0 s noblank") return def ws_subscribe(self): requested_updates = { "objects": { "bed_mesh": ["profile_name","mesh_max","mesh_min","probed_matrix"], "configfile": ["config"], "extruder": ["target","temperature","pressure_advance","smooth_time"], "fan": ["speed"], "gcode_move": ["extrude_factor","gcode_position","homing_origin","speed_factor"], "heater_bed": ["target","temperature"], "pause_resume": ["is_paused"], "print_stats": ["print_duration","total_duration","filament_used","filename","state","message"], "toolhead": ["homed_axes","estimated_print_time","print_time","position","extruder"], "virtual_sdcard": ["file_position","is_active","progress"], "webhooks": ["state","state_message"] } } self._ws.klippy.object_subscription(requested_updates) def _load_panel(self, panel, *args): if not panel in self.load_panel: logger.debug("Loading panel: %s" % panel) panel_path = os.path.join(os.path.dirname(__file__), 'panels', "%s.py" % panel) logger.info("Panel path: %s" % panel_path) if not os.path.exists(panel_path): msg = f"Panel {panel} does not exist" logger.info(msg) raise Exception(msg) module = importlib.import_module("panels.%s" % panel) if not hasattr(module, "create_panel"): msg = f"Cannot locate create_panel function for {panel}" logger.info(msg) raise Exception(msg) self.load_panel[panel] = getattr(module, "create_panel") try: return self.load_panel[panel](*args) except Exception: msg = f"Unable to create panel {panel}" logger.exception(msg) raise Exception(msg) def show_panel(self, panel_name, type, title, remove=None, pop=True, **kwargs): if panel_name not in self.panels: self.panels[panel_name] = self._load_panel(type, self, title) try: if kwargs != {}: self.panels[panel_name].initialize(panel_name, **kwargs) else: self.panels[panel_name].initialize(panel_name) except: del self.panels[panel_name] self.show_error_modal("Unable to load panel %s" % type) return if hasattr(self.panels[panel_name],"process_update"): self.panels[panel_name].process_update("notify_status_update", self.printer.get_data()) if hasattr(self.panels[panel_name],"activate"): self.panels[panel_name].activate() if remove == 2: self._remove_all_panels() elif remove == 1: self._remove_current_panel(pop) self.add(self.panels[panel_name].get()) self.show_all() self._cur_panels.append(panel_name) logger.debug("Current panel hierarchy: %s", str(self._cur_panels)) def show_popup_message(self, message): if self.popup_message != None: self.close_popup_message() box = Gtk.Box() box.get_style_context().add_class("message_popup") box.set_size_request(self.width, self.gtk.get_header_size()) label = Gtk.Label() if "must home axis first" in message.lower(): message = "Must home all axis first." label.set_text(message) close = Gtk.Button.new_with_label("X") close.set_can_focus(False) close.props.relief = Gtk.ReliefStyle.NONE close.connect("clicked", self.close_popup_message) box.pack_start(label, True, True, 0) box.pack_end(close, False, False, 0) box.set_halign(Gtk.Align.CENTER) cur_panel = self.panels[self._cur_panels[-1]] for i in ['back','estop','home']: if i in cur_panel.control: cur_panel.control[i].set_sensitive(False) cur_panel.get().put(box, 0,0) self.show_all() self.popup_message = box GLib.timeout_add(10000, self.close_popup_message) return False def close_popup_message(self, widget=None): if self.popup_message == None: return cur_panel = self.panels[self._cur_panels[-1]] for i in ['back','estop','home']: if i in cur_panel.control: cur_panel.control[i].set_sensitive(True) cur_panel.get().remove(self.popup_message) self.popup_message = None self.show_all() def show_error_modal(self, err): _ = self.lang.gettext logger.exception("Showing error modal: %s", err) buttons = [ {"name":_("Go Back"),"response": Gtk.ResponseType.CANCEL} ] label = Gtk.Label() label.set_markup(("%s \n\n" % err) + _("Check /tmp/KlipperScreen.log for more information.\nPlease submit an issue on GitHub for help.")) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) dialog = self.gtk.Dialog(self, buttons, label, self.error_modal_response) def error_modal_response(self, widget, response_id): widget.destroy() def init_style(self): style_provider = Gtk.CssProvider() #style_provider.load_from_path(klipperscreendir + "/style.css") css = open(klipperscreendir + "/styles/style.css") css_data = css.read() css.close() css_data = css_data.replace("KS_FONT_SIZE",str(self.gtk.get_font_size())) style_provider = Gtk.CssProvider() style_provider.load_from_data(css_data.encode()) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) def is_printing(self): return "job_status" in self._cur_panels def _go_to_submenu(self, widget, name): logger.info("#### Go to submenu " + str(name)) #self._remove_current_panel(False) # Find current menu item panels = list(self._cur_panels) if "job_status" not in self._cur_panels: menu = "__main" else: menu = "__print" logger.info("#### Menu " + str(menu)) disname = self._config.get_menu_name(menu, name) menuitems = self._config.get_menu_items(menu, name) if len(menuitems) == 0: logger.info("No items in menu, returning.") return self.show_panel(self._cur_panels[-1] + '_' + name, "menu", disname, 1, False, display_name=disname, items=menuitems) def _remove_all_panels(self): while len(self._cur_panels) > 0: self._remove_current_panel(True, False) self.show_all() def _remove_current_panel(self, pop=True, show=True): if len(self._cur_panels) > 0: self.remove(self.panels[self._cur_panels[-1]].get()) if pop == True: self._cur_panels.pop() if len(self._cur_panels) > 0: self.add(self.panels[self._cur_panels[-1]].get()) if show == True: self.show_all() def _menu_go_back (self, widget=None): logger.info("#### Menu go back") self._remove_current_panel() def _menu_go_home(self): logger.info("#### Menu go home") while len(self._cur_panels) > 1: self._remove_current_panel() def add_subscription (self, panel_name): add = True for sub in self.subscriptions: if sub == panel_name: return self.subscriptions.append(panel_name) def remove_subscription (self, panel_name): for i in range(len(self.subscriptions)): if self.subscriptions[i] == panel_name: self.subscriptions.pop(i) return def _websocket_callback(self, action, data): _ = self.lang.gettext #print(json.dumps([action, data], indent=2)) if action == "notify_klippy_disconnected": logger.info("### Going to disconnected state") self.printer_initializing(_("Klipper has shutdown")) return elif action == "notify_klippy_ready": logger.info("### Going to ready state") self.init_printer() elif action == "notify_status_update" and self.shutdown == False: self.printer.process_update(data) if "webhooks" in data: print(json.dumps([action, data], indent=2)) if "webhooks" in data and "state" in data['webhooks']: if data['webhooks']['state'] == "ready": logger.info("### Going to ready state") self.printer_ready() elif data['webhooks']['state'] == "shutdown": self.shutdown == True self.printer_initializing(_("Klipper has shutdown")) else: active = self.printer.get_stat('virtual_sdcard','is_active') paused = self.printer.get_stat('pause_resume','is_paused') if "job_status" not in self._cur_panels: if active == True or paused == True: self.printer_printing() elif action == "notify_filelist_changed": logger.debug("Filelist changed: %s", json.dumps(data,indent=2)) #self.files.add_file() elif action == "notify_metadata_update": self.files.request_metadata(data['filename']) elif action == "notify_power_changed": logger.debug("Power status changed: %s", data) self.printer.process_power_update(data) elif self.shutdown == False and action == "notify_gcode_response": if "Klipper state: Shutdown" in data: self.shutdown == True self.printer_initializing(_("Klipper has shutdown")) if not (data.startswith("B:") and re.search(r'B:[0-9\.]+\s/[0-9\.]+\sT[0-9]+:[0-9\.]+', data)): if data.startswith("!! "): self.show_popup_message(data[3:]) logger.debug(json.dumps([action, data], indent=2)) for sub in self.subscriptions: self.panels[sub].process_update(action, data) def _confirm_send_action(self, widget, text, method, params): _ = self.lang.gettext buttons = [ {"name":_("Continue"), "response": Gtk.ResponseType.OK}, {"name":_("Cancel"),"response": Gtk.ResponseType.CANCEL} ] label = Gtk.Label() label.set_markup(text) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) dialog = self.gtk.Dialog(self, buttons, label, self._confirm_send_action_response, method, params) def _confirm_send_action_response(self, widget, response_id, method, params): if response_id == Gtk.ResponseType.OK: self._send_action(widget, method, params) widget.destroy() def _send_action(self, widget, method, params): self._ws.send_method(method, params) def printer_initializing(self, text=None): self.shutdown = True self.close_popup_message() self.show_panel('splash_screen',"splash_screen", "Splash Screen", 2) if text != None: self.panels['splash_screen'].update_text(text) self.panels['splash_screen'].show_restart_buttons() def init_printer(self): _ = self.lang.gettext self.shutdown = False status_objects = [ 'bed_mesh', 'idle_timeout', 'configfile', 'gcode_move', 'fan', 'toolhead', 'virtual_sdcard', 'print_stats', 'heater_bed', 'extruder', 'pause_resume' ] printer_info = self.apiclient.get_printer_info() data = self.apiclient.send_request("printer/objects/query?" + "&".join(status_objects)) powerdevs = self.apiclient.send_request("machine/device_power/devices") if printer_info == False or data == False: self.printer_initializing(_("Moonraker error")) return data = data['result']['status'] # Reinitialize printer, in case the printer was shut down and anything has changed. self.printer.__init__(printer_info['result'], data) self.ws_subscribe() if powerdevs != False: self.printer.configure_power_devices(powerdevs['result']) if self.files == None: self.files = KlippyFiles(self) else: self.files.add_timeout() if printer_info['result']['state'] in ("error","shutdown","startup"): if printer_info['result']['state'] == "startup": self.printer_initializing(_("Klipper is attempting to start")) elif "FIRMWARE_RESTART" in printer_info['result']['state_message']: self.printer_initializing( _("Klipper has encountered an error.\nIssue a FIRMWARE_RESTART to attempt fixing the issue.") ) else: self.printer_initializing(_("Klipper has shutdown")) return if (data['print_stats']['state'] == "printing" or data['print_stats']['state'] == "paused"): filename = self.printer.get_stat("print_stats","filename") if not self.files.file_metadata_exists(filename): self.files.request_metadata(filename) self.printer_printing() return self.printer_ready() def printer_ready(self): if self.shutdown == True: self.init_printer() return self.files.add_timeout() self.close_popup_message() self.show_panel('main_panel', "main_menu", "Main Menu", 2, items=self._config.get_menu_items("__main"), extrudercount=self.printer.get_extruder_count()) if "job_status" in self.panels: self.remove_subscription("job_status") del self.panels["job_status"] def printer_printing(self): self.ws_subscribe() self.files.remove_timeout() self.close_popup_message() self.show_panel('job_status',"job_status", "Print Status", 2)
def __init__(self): self.version = get_software_version() logger.info("KlipperScreen version: %s" % self.version) parser = argparse.ArgumentParser( description="KlipperScreen - A GUI for Klipper") parser.add_argument( "-c", "--configfile", default="~/KlipperScreen.conf", metavar='<configfile>', help="Location of KlipperScreen configuration file") args = parser.parse_args() configfile = os.path.normpath(os.path.expanduser(args.configfile)) self._config = KlipperScreenConfig(configfile) self.printer = Printer({"software_version": "Unknown"}, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }) self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, "printing": self.state_printing, "ready": self.state_ready, "startup": self.state_startup, "shutdown": self.state_shutdown }) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales', fallback=True) _ = self.lang.gettext self.apiclient = KlippyRest( self._config.get_main_config_option("moonraker_host"), self._config.get_main_config_option("moonraker_port"), self._config.get_main_config_option("moonraker_api_key", False)) Gtk.Window.__init__(self) self.width = self._config.get_main_config().getint( "width", Gdk.Screen.get_width(Gdk.Screen.get_default())) self.height = self._config.get_main_config().getint( "height", Gdk.Screen.get_height(Gdk.Screen.get_default())) self.set_default_size(self.width, self.height) self.set_resizable(False) logger.info("Screen resolution: %sx%s" % (self.width, self.height)) self.gtk = KlippyGtk(self.width, self.height) self.init_style() #self._load_panels() self.printer_initializing(_("Initializing")) self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }, self._config.get_main_config_option("moonraker_host"), self._config.get_main_config_option("moonraker_port")) self._ws.initial_connect() # Disable DPMS os.system("/usr/bin/xset -display :0 s off") os.system("/usr/bin/xset -display :0 -dpms") os.system("/usr/bin/xset -display :0 s noblank") return
class KlipperScreen(Gtk.Window): """ Class for creating a screen for Klipper via HDMI """ currentPanel = None bed_temp_label = None number_tools = 1 panels = {} _cur_panels = [] files = None filename = "" subscriptions = [] last_update = {} shutdown = True printer = None def __init__(self): self._config = KlipperScreenConfig() self.init_style() self.printer = Printer({ 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }) self.lang = gettext.translation('KlipperScreen', localedir='ks_includes/locales') _ = self.lang.gettext self.apiclient = KlippyRest("127.0.0.1", 7125) Gtk.Window.__init__(self) self.width = Gdk.Screen.get_width(Gdk.Screen.get_default()) self.height = Gdk.Screen.get_height(Gdk.Screen.get_default()) self.set_default_size(self.width, self.height) self.set_resizable(False) self.version = get_software_version() logger.info("KlipperScreen version: %s" % self.version) logger.info("Screen resolution: %sx%s" % (self.width, self.height)) self.printer_initializing(_("Initializing")) self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }) self._ws.connect() # Disable DPMS os.system("/usr/bin/xset -display :0 s off") os.system("/usr/bin/xset -display :0 -dpms") os.system("/usr/bin/xset -display :0 s noblank") return def ws_subscribe(self): requested_updates = { "objects": { "configfile": ["config"], "extruder": ["target", "temperature", "pressure_advance", "smooth_time"], "fan": ["speed"], "gcode_move": ["homing_origin", "extrude_factor", "speed_factor"], "heater_bed": ["target", "temperature"], "print_stats": [ "print_duration", "total_duration", "filament_used", "filename", "state", "message" ], "toolhead": [ "homed_axes", "estimated_print_time", "print_time", "position", "extruder" ], "virtual_sdcard": ["file_position", "is_active", "progress"], "webhooks": ["state", "state_message"] } } self._ws.klippy.object_subscription(requested_updates) def show_panel(self, panel_name, type, remove=None, pop=True, **kwargs): if panel_name not in self.panels: try: if type == "SplashScreenPanel": self.panels[panel_name] = SplashScreenPanel(self) elif type == "MainPanel": self.panels[panel_name] = MainPanel(self) elif type == "menu": self.panels[panel_name] = MenuPanel(self) elif type == "bed_level": self.panels[panel_name] = BedLevelPanel(self) elif type == "extrude": self.panels[panel_name] = ExtrudePanel(self) elif type == "finetune": self.panels[panel_name] = FineTune(self) elif type == "JobStatusPanel": self.panels[panel_name] = JobStatusPanel(self) elif type == "move": self.panels[panel_name] = MovePanel(self) elif type == "network": self.panels[panel_name] = NetworkPanel(self) elif type == "preheat": self.panels[panel_name] = PreheatPanel(self) elif type == "print": self.panels[panel_name] = PrintPanel(self) elif type == "temperature": self.panels[panel_name] = TemperaturePanel(self) elif type == "fan": self.panels[panel_name] = FanPanel(self) elif type == "system": self.panels[panel_name] = SystemPanel(self) elif type == "zcalibrate": self.panels[panel_name] = ZCalibratePanel(self) #Temporary for development else: self.panels[panel_name] = MovePanel(self) except: self.show_error_modal("Unable to load panel %s" % panel_name) return try: if kwargs != {}: self.panels[panel_name].initialize(panel_name, **kwargs) else: self.panels[panel_name].initialize(panel_name) except: del self.panels[panel_name] self.show_error_modal("Unable to load panel %s" % panel_name) return if hasattr(self.panels[panel_name], "process_update"): self.panels[panel_name].process_update(self.printer.get_data()) if hasattr(self.panels[panel_name], "activate"): self.panels[panel_name].activate() if remove == 2: self._remove_all_panels() elif remove == 1: self._remove_current_panel(pop) self.add(self.panels[panel_name].get()) self.show_all() self._cur_panels.append(panel_name) logger.debug("Current panel hierarchy: %s", str(self._cur_panels)) def show_error_modal(self, err): _ = self.lang.gettext logger.exception("Showing error modal: %s", err) buttons = [{"name": _("Go Back"), "response": Gtk.ResponseType.CANCEL}] label = Gtk.Label() label.set_markup(("%s \n\n" % err) + _( "Check /tmp/KlipperScreen.log for more information.\nPlease submit an issue on GitHub for help." )) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) label.get_style_context().add_class("text") dialog = KlippyGtk.Dialog(self, buttons, label, self.error_modal_response) def error_modal_response(self, widget, response_id): widget.destroy() def init_style(self): style_provider = Gtk.CssProvider() style_provider.load_from_path(klipperscreendir + "/style.css") Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def _go_to_submenu(self, widget, name): logger.info("#### Go to submenu " + str(name)) #self._remove_current_panel(False) # Find current menu item panels = list(self._cur_panels) if "job_status" not in self._cur_panels: menu = "__main" else: menu = "__print" logger.info("#### Menu " + str(menu)) #self.show_panel("_".join(self._cur_panels) + '_' + name, "menu", 1, False, menu=menu) menuitems = self._config.get_menu_items(menu, name) if len(menuitems) == 0: logger.info("No items in menu, returning.") return self.show_panel(self._cur_panels[-1] + '_' + name, "menu", 1, False, items=menuitems) return grid = self.arrangeMenuItems(menu, 4) b = KlippyGtk.ButtonImage('back', 'Back') b.connect("clicked", self._menu_go_back) grid.attach(b, 4, 2, 1, 1) self._cur_panels.append(cur_item['name']) #str(cur_item['name'])) self.panels[cur_item['name']] = grid self.add(self.panels[cur_item['name']]) self.show_all() def _remove_all_panels(self): while len(self._cur_panels) > 0: self._remove_current_panel() def _remove_current_panel(self, pop=True): if len(self._cur_panels) > 0: self.remove(self.panels[self._cur_panels[-1]].get()) if pop == True: self._cur_panels.pop() if len(self._cur_panels) > 0: self.add(self.panels[self._cur_panels[-1]].get()) self.show_all() def _menu_go_back(self, widget): logger.info("#### Menu go back") self._remove_current_panel() def add_subscription(self, panel_name): add = True for sub in self.subscriptions: if sub == panel_name: return self.subscriptions.append(panel_name) def remove_subscription(self, panel_name): for i in range(len(self.subscriptions)): if self.subscriptions[i] == panel_name: self.subscriptions.pop(i) return def _websocket_callback(self, action, data): _ = self.lang.gettext #print(json.dumps([action, data], indent=2)) if action == "notify_klippy_disconnected": logger.info("### Going to disconnected state") self.printer_initializing(_("Klipper has shutdown")) return elif action == "notify_klippy_ready": logger.info("### Going to ready state") self.init_printer() elif action == "notify_status_update" and self.shutdown == False: self.printer.process_update(data) if "webhooks" in data: print(json.dumps([action, data], indent=2)) if "webhooks" in data and "state" in data['webhooks']: if data['webhooks']['state'] == "ready": logger.info("### Going to ready state") self.printer_ready() elif data['webhooks']['state'] == "shutdown": self.shutdown == True self.printer_initializing(_("Klipper has shutdown")) else: active = self.printer.get_stat('virtual_sdcard', 'is_active') paused = self.printer.get_stat('pause_resume', 'is_paused') if "job_status" in self._cur_panels: if active == False and paused == False: self.printer_ready() else: if active == True or paused == True: self.printer_printing() elif action == "notify_filelist_changed": logger.debug("Filelist changed: %s", json.dumps(data, indent=2)) #self.files.add_file() elif action == "notify_metadata_update": self.files.update_metadata(data['filename']) elif self.shutdown == False and not ( action == "notify_gcode_response" and data.startswith("B:") and re.search(r'B:[0-9\.]+\s/[0-9\.]+\sT[0-9]+:[0-9\.]+', data)): logger.debug(json.dumps([action, data], indent=2)) for sub in self.subscriptions: self.panels[sub].process_update(data) def _confirm_send_action(self, widget, text, method, params): _ = self.lang.gettext buttons = [{ "name": _("Continue"), "response": Gtk.ResponseType.OK }, { "name": _("Cancel"), "response": Gtk.ResponseType.CANCEL }] label = Gtk.Label() label.set_markup(text) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) label.get_style_context().add_class("text") dialog = KlippyGtk.Dialog(self, buttons, label, self._confirm_send_action_response, method, params) def _confirm_send_action_response(self, widget, response_id, method, params): if response_id == Gtk.ResponseType.OK: self._send_action(widget, method, params) widget.destroy() def _send_action(self, widget, method, params): self._ws.send_method(method, params) def printer_initializing(self, text=None): self.shutdown = True self.show_panel('splash_screen', "SplashScreenPanel", 2) if text != None: self.panels['splash_screen'].update_text(text) self.panels['splash_screen'].show_restart_buttons() def init_printer(self): _ = self.lang.gettext self.shutdown = False status_objects = [ 'idle_timeout', 'configfile', 'gcode_move', 'fan', 'toolhead', 'virtual_sdcard', 'print_stats', 'heater_bed', 'extruder', 'pause_resume' ] info = self.apiclient.get_printer_info() data = self.apiclient.send_request("printer/objects/query?" + "&".join(status_objects)) if info == False or data == False: self.printer_initializing(_("Moonraker error")) return data = data['result']['status'] # Reinitialize printer, in case the printer was shut down and anything has changed. self.printer.__init__(data) self.ws_subscribe() if self.files == None: self.files = KlippyFiles(self) else: self.files.add_timeout() if info['result']['state'] == "shutdown": if "FIRMWARE_RESTART" in info['result']['state_message']: self.printer_initializing( _("Klipper has encountered an error. Issue a FIRMWARE_RESTART to attempt fixing the issue." )) else: self.printer_initializing(_("Klipper has shutdown")) return if (data['print_stats']['state'] == "printing" or data['print_stats']['state'] == "paused"): self.printer_printing() return self.printer_ready() def printer_ready(self): if self.shutdown == True: self.init_printer() return self.files.add_timeout() self.show_panel('main_panel', "MainPanel", 2, items=self._config.get_menu_items("__main"), extrudercount=self.printer.get_extruder_count()) def printer_printing(self): self.ws_subscribe() self.files.remove_timeout() self.show_panel('job_status', "JobStatusPanel", 2)
class KlipperScreen(Gtk.Window): """ Class for creating a screen for Klipper via HDMI """ _cur_panels = [] bed_temp_label = None connecting = False connecting_to_printer = None connected_printer = None currentPanel = None files = None filename = "" keyboard = None last_update = {} load_panel = {} number_tools = 1 panels = {} popup_message = None screensaver = None printer = None printer_select_callbacks = [] printer_select_prepanel = None rtl_languages = ['he_il'] subscriptions = [] shutdown = True updating = False update_queue = [] _ws = None init_printer_timeout = None dpms_timeout = None screensaver_timeout = None reinit_count = 0 def __init__(self, args, version): self.blanking_time = 600 self.use_dpms = True self.apiclient = None self.version = version self.dialogs = [] self.confirm = None configfile = os.path.normpath(os.path.expanduser(args.configfile)) self._config = KlipperScreenConfig(configfile, self) self.lang = self._config.get_lang() logging.debug(f"OS Language: {os.getenv('LANG')}") self.lang_ltr = True for lang in self.rtl_languages: if os.getenv('LANG').lower().startswith(lang): self.lang_ltr = False Gtk.Widget.set_default_direction(Gtk.TextDirection.RTL) logging.debug("Enabling RTL mode") break Gtk.Window.__init__(self) monitor = Gdk.Display.get_default().get_primary_monitor() self.width = self._config.get_main_config().getint( "width", monitor.get_geometry().width) self.height = self._config.get_main_config().getint( "height", monitor.get_geometry().height) self.set_default_size(self.width, self.height) self.set_resizable(False) self.vertical_mode = self.width < self.height logging.info(f"Screen resolution: {self.width}x{self.height}") self.theme = self._config.get_main_config().get('theme') self.show_cursor = self._config.get_main_config().getboolean( "show_cursor", fallback=False) self.gtk = KlippyGtk( self, self.width, self.height, self.theme, self.show_cursor, self._config.get_main_config().get("font_size", "medium")) self.init_style() self.base_panel = BasePanel(self, title="Base Panel", back=False) self.add(self.base_panel.get()) self.show_all() self.base_panel.activate() self.printer_initializing(_("Initializing")) self.set_screenblanking_timeout( self._config.get_main_config().get('screen_blanking')) # Move mouse to 0,0 os.system("/usr/bin/xdotool mousemove 0 0") self.change_cursor() self.initial_connection() def initial_connection(self): printers = self._config.get_printers() default_printer = self._config.get_main_config().get('default_printer') logging.debug(f"Default printer: {default_printer}") if [True for p in printers if default_printer in p]: self.connect_printer(default_printer) elif len(printers) == 1: pname = list(printers[0])[0] self.connect_printer(pname) else: self.show_panel("printer_select", "printer_select", "Printer Select", 2) def connect_printer_widget(self, widget, name): self.connect_printer(name) def connect_printer(self, name): data = { "moonraker_host": "127.0.0.1", "moonraker_port": "7125", "moonraker_api_key": False } self.connecting_to_printer = name if self.connected_printer == name: if self.printer_select_prepanel is not None: self.show_panel(self.printer_select_prepanel, "", "", 2) self.printer_select_prepanel = None while len(self.printer_select_callbacks) > 0: i = self.printer_select_callbacks.pop(0) i() if self.printer.get_state() not in [ "disconnected", "error", "startup", "shutdown" ]: self.base_panel.show_heaters(True) self.base_panel.show_printer_select(True) self.base_panel.show_macro_shortcut( self._config.get_main_config().getboolean( 'side_macro_shortcut')) return # Cleanup self.printer_select_callbacks = [] self.printer_select_prepanel = None if self.files is not None: self.files.reset() self.files = None if self.printer is not None: self.printer.reset() self.printer = None for printer in self._config.get_printers(): pname = list(printer)[0] if pname != name: continue data = printer[pname] break if self._ws is not None: self._ws.close() self.connecting = True logging.info(f"Connecting to printer: {name}") self.apiclient = KlippyRest(data["moonraker_host"], data["moonraker_port"], data["moonraker_api_key"]) self.printer = Printer({"software_version": "Unknown"}, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }, self.state_execute) self._remove_all_panels() self.subscriptions = [] for panel in list(self.panels): if panel not in ["printer_select", "splash_screen"]: del self.panels[panel] for dialog in self.dialogs: dialog.destroy() self.base_panel.show_printer_select(True) self.printer_initializing(_("Connecting to %s") % name) self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, "paused": self.state_paused, "printing": self.state_printing, "ready": self.state_ready, "startup": self.state_startup, "shutdown": self.state_shutdown }) self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }, data["moonraker_host"], data["moonraker_port"]) self.files = KlippyFiles(self) self._ws.initial_connect() self.connecting = False self.connected_printer = name logging.debug(f"Connected to printer: {name}") def ws_subscribe(self): requested_updates = { "objects": { "bed_mesh": ["profile_name", "mesh_max", "mesh_min", "probed_matrix"], "configfile": ["config"], "display_status": ["progress", "message"], "fan": ["speed"], "gcode_move": [ "extrude_factor", "gcode_position", "homing_origin", "speed_factor", "speed" ], "idle_timeout": ["state"], "pause_resume": ["is_paused"], "print_stats": [ "print_duration", "total_duration", "filament_used", "filename", "state", "message" ], "toolhead": [ "homed_axes", "estimated_print_time", "print_time", "position", "extruder", "max_accel", "max_accel_to_decel", "max_velocity", "square_corner_velocity" ], "virtual_sdcard": ["file_position", "is_active", "progress"], "webhooks": ["state", "state_message"], "firmware_retraction": [ "retract_length", "retract_speed", "unretract_extra_length", "unretract_speed" ], "motion_report": ["live_position", "live_velocity", "live_extruder_velocity"] } } for extruder in self.printer.get_tools(): requested_updates['objects'][extruder] = [ "target", "temperature", "pressure_advance", "smooth_time" ] for h in self.printer.get_heaters(): requested_updates['objects'][h] = ["target", "temperature"] for f in self.printer.get_fans(): requested_updates['objects'][f] = ["speed"] for f in self.printer.get_filament_sensors(): requested_updates['objects'][f] = ["enabled", "filament_detected"] for p in self.printer.get_output_pins(): requested_updates['objects'][p] = ["value"] self._ws.klippy.object_subscription(requested_updates) def _load_panel(self, panel, *args): if panel not in self.load_panel: logging.debug(f"Loading panel: {panel}") panel_path = os.path.join(os.path.dirname(__file__), 'panels', f"{panel}.py") logging.info(f"Panel path: {panel_path}") if not os.path.exists(panel_path): msg = f"Panel {panel} does not exist" logging.info(msg) raise Exception(msg) module = importlib.import_module(f"panels.{panel}") if not hasattr(module, "create_panel"): msg = f"Cannot locate create_panel function for {panel}" logging.info(msg) raise Exception(msg) self.load_panel[panel] = getattr(module, "create_panel") try: return self.load_panel[panel](*args) except Exception as e: msg = f"Unable to create panel {panel}\n{e}" logging.exception(msg) raise Exception(msg) from e def show_panel(self, panel_name, panel_type, title, remove=None, pop=True, **kwargs): if panel_name not in self.panels: try: self.panels[panel_name] = self._load_panel( panel_type, self, title) if kwargs != {}: self.panels[panel_name].initialize(panel_name, **kwargs) else: self.panels[panel_name].initialize(panel_name) except Exception as e: if panel_name in self.panels: del self.panels[panel_name] logging.exception(f"Unable to load panel {panel_type}") self.show_error_modal(f"Unable to load panel {panel_type}", f"{e}") return if hasattr(self.panels[panel_name], "process_update"): self.panels[panel_name].process_update("notify_status_update", self.printer.get_data()) try: if remove == 2: self._remove_all_panels() elif remove == 1: self._remove_current_panel(pop) logging.debug(f"Attaching panel {panel_name}") self.base_panel.add_content(self.panels[panel_name]) logging.debug(f"Showing back. count: {len(self._cur_panels)}") if len(self._cur_panels) == 0: self.base_panel.show_back(False) else: self.base_panel.show_back(True) self.show_all() if hasattr(self.panels[panel_name], "process_update"): self.panels[panel_name].process_update( "notify_status_update", self.printer.get_updates()) self.add_subscription(panel_name) if hasattr(self.panels[panel_name], "activate"): self.panels[panel_name].activate() self.show_all() except Exception as e: logging.exception(f"Error attaching panel:\n{e}") self._cur_panels.append(panel_name) logging.debug(f"Current panel hierarchy: {self._cur_panels}") def show_popup_message(self, message, level=3): if self.screensaver is not None: self.wake_screen() if self.popup_message is not None: self.close_popup_message() msg = Gtk.Button(label=f"{message}") msg.set_hexpand(True) msg.set_vexpand(True) msg.get_child().set_line_wrap(True) msg.get_child().set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) msg.connect("clicked", self.close_popup_message) close = Gtk.Button(label="<b><big>X</big></b>") close.set_hexpand(False) close.set_vexpand(True) close.get_child().set_use_markup(True) close.set_can_focus(False) close.connect("clicked", self.close_popup_message) box = Gtk.Box() box.set_size_request(self.width, -1) box.set_halign(Gtk.Align.CENTER) box.get_style_context().add_class("message_popup") if level == 1: box.get_style_context().add_class("message_popup_echo") elif level == 2: box.get_style_context().add_class("message_popup_warning") else: box.get_style_context().add_class("message_popup_error") box.add(msg) box.add(close) self.base_panel.get().put(box, 0, 0) self.show_all() self.popup_message = box if self._config.get_main_config().getboolean('autoclose_popups'): GLib.timeout_add_seconds(10, self.close_popup_message) return False def close_popup_message(self, widget=None): if self.popup_message is None: return self.base_panel.get().remove(self.popup_message) self.popup_message = None self.show_all() def show_error_modal(self, err, e=""): logging.exception(f"Showing error modal: {err}") title = Gtk.Label() title.set_markup(f"<b>{err}</b>\n\n") title.set_line_wrap(True) title.set_halign(Gtk.Align.START) message = Gtk.Label() message.set_markup( "Provide /tmp/KlipperScreen.log when asking for help.\n\n" + f"KlipperScreen: {self.version}\n" + f"<i>{e}</i>\n") message.set_line_wrap(True) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) vbox.set_halign(Gtk.Align.CENTER) vbox.set_valign(Gtk.Align.CENTER) vbox.add(title) vbox.add(message) scroll = self.gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scroll.add(vbox) buttons = [{"name": _("Go Back"), "response": Gtk.ResponseType.CANCEL}] self.gtk.Dialog(self, buttons, scroll, self.error_modal_response) @staticmethod def error_modal_response(widget, response_id): widget.destroy() def restart_warning(self, value): logging.debug(f"Showing restart warning because: {value}") buttons = [{ "name": _("Cancel"), "response": Gtk.ResponseType.CANCEL }, { "name": _("Restart"), "response": Gtk.ResponseType.OK }] label = Gtk.Label() label.set_markup( _("To apply %s KlipperScreen needs to be restarted") % value) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_vexpand(True) label.set_valign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) self.gtk.Dialog(self, buttons, label, self.restart_ks) def restart_ks(self, widget, response_id): if response_id == Gtk.ResponseType.OK: logging.debug("Restarting") # This can be removed after a grace period service = self._config.get_main_config().get('service') if service is not None and service != "KlipperScreen": self.show_popup_message( "Error: option \"service\" is not supported anymore") # ^^^ self._ws.send_method("machine.services.restart", {"service": "KlipperScreen"}) widget.destroy() def init_style(self): css_data = pathlib.Path( os.path.join(klipperscreendir, "styles", "base.css")).read_text() with open(os.path.join(klipperscreendir, "styles", "base.conf")) as f: style_options = json.load(f) # Load custom theme theme = os.path.join(klipperscreendir, "styles", self.theme) theme_style = os.path.join(theme, "style.css") theme_style_conf = os.path.join(theme, "style.conf") if os.path.exists(theme_style): with open(theme_style) as css: css_data += css.read() if os.path.exists(theme_style_conf): try: with open(theme_style_conf) as f: style_options.update(json.load(f)) except Exception as e: logging.error( f"Unable to parse custom template conf file:\n{e}") self.gtk.color_list = style_options['graph_colors'] for i in range(len( style_options['graph_colors']['extruder']['colors'])): num = "" if i == 0 else i css_data += "\n.graph_label_extruder%s {border-left-color: #%s}" % ( num, style_options['graph_colors']['extruder']['colors'][i]) for i in range(len(style_options['graph_colors']['bed']['colors'])): css_data += "\n.graph_label_heater_bed%s {border-left-color: #%s}" % ( "" if i == 0 else i + 1, style_options['graph_colors']['bed']['colors'][i]) for i in range(len(style_options['graph_colors']['fan']['colors'])): css_data += "\n.graph_label_fan_%s {border-left-color: #%s}" % ( i + 1, style_options['graph_colors']['fan']['colors'][i]) for i in range(len(style_options['graph_colors']['sensor']['colors'])): css_data += "\n.graph_label_sensor_%s {border-left-color: #%s}" % ( i + 1, style_options['graph_colors']['sensor']['colors'][i]) css_data = css_data.replace("KS_FONT_SIZE", f"{self.gtk.get_font_size()}") style_provider = Gtk.CssProvider() style_provider.load_from_data(css_data.encode()) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def is_printing(self): return self.printer.get_state() == "printing" def is_updating(self): return self.updating def _go_to_submenu(self, widget, name): logging.info(f"#### Go to submenu {name}") # self._remove_current_panel(False) # Find current menu item if "main_panel" in self._cur_panels: menu = "__main" elif "splash_screen" in self._cur_panels: menu = "__splashscreen" else: menu = "__print" logging.info(f"#### Menu {menu}") disname = self._config.get_menu_name(menu, name) menuitems = self._config.get_menu_items(menu, name) if len(menuitems) == 0: logging.info("No items in menu, returning.") return self.show_panel(f'{self._cur_panels[-1]}_{name}', "menu", disname, 1, False, display_name=disname, items=menuitems) def _remove_all_panels(self): while len(self._cur_panels) > 0: self._remove_current_panel(True, False) self.show_all() def _remove_current_panel(self, pop=True, show=True): if len(self._cur_panels) <= 0: return self.base_panel.remove(self.panels[self._cur_panels[-1]].get_content()) if hasattr(self.panels[self._cur_panels[-1]], "deactivate"): self.panels[self._cur_panels[-1]].deactivate() self.remove_subscription(self._cur_panels[-1]) if pop is True: self._cur_panels.pop() if len(self._cur_panels) > 0: self.base_panel.add_content(self.panels[self._cur_panels[-1]]) self.base_panel.show_back(len(self._cur_panels) != 1) if hasattr(self.panels[self._cur_panels[-1]], "activate"): self.panels[self._cur_panels[-1]].activate() if hasattr(self.panels[self._cur_panels[-1]], "process_update"): self.panels[self._cur_panels[-1]].process_update( "notify_status_update", self.printer.get_updates()) self.add_subscription(self._cur_panels[-1]) if show is True: self.show_all() def _menu_go_back(self, widget=None): logging.info("#### Menu go back") self.remove_keyboard() if self._config.get_main_config().getboolean('autoclose_popups'): self.close_popup_message() self._remove_current_panel() def _menu_go_home(self): logging.info("#### Menu go home") self.remove_keyboard() self.close_popup_message() while len(self._cur_panels) > 1: self._remove_current_panel() def add_subscription(self, panel_name): for sub in self.subscriptions: if sub == panel_name: return self.subscriptions.append(panel_name) def remove_subscription(self, panel_name): for i in range(len(self.subscriptions)): if self.subscriptions[i] == panel_name: self.subscriptions.pop(i) return def reset_screensaver_timeout(self, widget=None): if self.screensaver_timeout is not None: GLib.source_remove(self.screensaver_timeout) self.screensaver_timeout = GLib.timeout_add_seconds( self.blanking_time, self.show_screensaver) def show_screensaver(self): logging.debug("Showing Screensaver") if self.screensaver is not None: self.close_screensaver() self.remove_keyboard() close = Gtk.Button() close.connect("clicked", self.close_screensaver) box = Gtk.Box() box.set_size_request(self.width, self.height) box.pack_start(close, True, True, 0) box.set_halign(Gtk.Align.CENTER) box.get_style_context().add_class("screensaver") self.base_panel.get().put(box, 0, 0) self.show_all() # Avoid leaving a cursor-handle close.grab_focus() self.screensaver = box return False def close_screensaver(self, widget=None): if self.screensaver is None: return False logging.debug("Closing Screensaver") self.base_panel.get().remove(self.screensaver) self.screensaver = None if self.use_dpms: self.wake_screen() else: self.screensaver_timeout = GLib.timeout_add_seconds( self.blanking_time, self.show_screensaver) self.show_all() return False def check_dpms_state(self): state = functions.get_DPMS_state() if state == functions.DPMS_State.Fail: logging.info("DPMS State FAIL: Stopping DPMS Check") if self.screensaver_timeout is None: self.set_dpms(False) return False elif state != functions.DPMS_State.On: if self.screensaver is None: self.show_screensaver() return True def wake_screen(self): # Wake the screen (it will go to standby as configured) if self._config.get_main_config().get('screen_blanking') != "off": logging.debug("Screen wake up") os.system("xset -display :0 dpms force on") self.close_screensaver() def set_dpms(self, use_dpms): self.use_dpms = use_dpms logging.info(f"DPMS set to: {self.use_dpms}") self.set_screenblanking_timeout( self._config.get_main_config().get('screen_blanking')) def set_screenblanking_timeout(self, time): os.system("xset -display :0 s noblank") os.system("xset -display :0 s off") self.use_dpms = self._config.get_main_config().getboolean( "use_dpms", fallback=True) if time == "off": logging.debug(f"Screen blanking: {time}") if self.dpms_timeout is not None: GLib.source_remove(self.dpms_timeout) self.dpms_timeout = None if self.screensaver_timeout is not None: GLib.source_remove(self.screensaver_timeout) os.system("xset -display :0 dpms 0 0 0") return self.blanking_time = abs(int(time)) logging.debug(f"Changing screen blanking to: {self.blanking_time}") if self.use_dpms and functions.dpms_loaded is True: os.system("xset -display :0 +dpms") if functions.get_DPMS_state() == functions.DPMS_State.Fail: logging.info("DPMS State FAIL") else: logging.debug("Using DPMS") os.system("xset -display :0 s off") os.system(f"xset -display :0 dpms 0 {self.blanking_time} 0") if self.dpms_timeout is None: self.dpms_timeout = GLib.timeout_add_seconds( 1, self.check_dpms_state) return # Without dpms just blank the screen logging.debug("Not using DPMS") if self.dpms_timeout is not None: GLib.source_remove(self.dpms_timeout) self.dpms_timeout = None os.system("xset -display :0 dpms 0 0 0") if self.screensaver_timeout is None: self.screensaver_timeout = GLib.timeout_add_seconds( self.blanking_time, self.show_screensaver) return def set_updating(self, updating=False): if self.updating is True and updating is False and len( self.update_queue) > 0: i = self.update_queue.pop() self.update_queue = [] i[0](i[1]) self.updating = updating def show_printer_select(self, widget=None): logging.debug(f"Saving panel: {self._cur_panels[0]}") self.printer_select_prepanel = self._cur_panels[0] self.base_panel.show_heaters(False) self.base_panel.show_macro_shortcut(False) self.base_panel.show_printer_select(False) self.show_panel("printer_select", "printer_select", "Printer Select", 2) self.show_all() self.base_panel.action_bar.hide() def state_execute(self, callback, prev_state): if self.is_updating(): self.update_queue.append([callback, prev_state]) else: callback(prev_state) def state_disconnected(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_disconnected] return logging.debug("### Going to disconnected") self.base_panel.show_macro_shortcut(False) self.wake_screen() self.printer_initializing(_("Klipper has disconnected")) if self.connected_printer is not None: self.connected_printer = None # Try to reconnect self.connect_printer(self.connecting_to_printer) else: self.initial_connection() def state_error(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_error] return self.base_panel.show_macro_shortcut(False) self.wake_screen() msg = self.printer.get_stat("webhooks", "state_message") if "FIRMWARE_RESTART" in msg: self.printer_initializing( "<b>" + _("Klipper has encountered an error.") + "\n" + _("A FIRMWARE_RESTART may fix the issue.") + "</b>" + "\n\n" + msg) elif "micro-controller" in msg: self.printer_initializing( "<b>" + _("Klipper has encountered an error.") + _("Please recompile and flash the micro-controller.") + "</b>" + "\n\n" + msg) else: self.printer_initializing("<b>" + _("Klipper has encountered an error.") + "</b>" + "\n\n" + msg) for panel in list(self.panels): if panel not in ["printer_select", "splash_screen"]: del self.panels[panel] for dialog in self.dialogs: dialog.destroy() def state_paused(self, prev_state): if "job_status" not in self._cur_panels: self.printer_printing() def state_printing(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_printing] return if "job_status" not in self._cur_panels: self.printer_printing() else: self.panels["job_status"].new_print() def state_ready(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_ready] return self.base_panel.show_macro_shortcut( self._config.get_main_config().getboolean('side_macro_shortcut')) self.base_panel.show_heaters(True) # Do not return to main menu if completing a job, timeouts/user input will return if "job_status" in self._cur_panels or "main_menu" in self._cur_panels: return if prev_state not in ['paused', 'printing']: self.init_printer() self.base_panel._printer = self.printer self.printer_ready() def state_startup(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_startup] return self.printer_initializing(_("Klipper is attempting to start")) def state_shutdown(self, prev_state): if "printer_select" in self._cur_panels: self.printer_select_callbacks = [self.state_shutdown] return self.base_panel.show_macro_shortcut(False) self.wake_screen() msg = self.printer.get_stat("webhooks", "state_message") if "ready" in msg: msg = "" self.printer_initializing("<b>" + _("Klipper has shutdown") + "</b>" + "\n\n" + msg) def toggle_macro_shortcut(self, value): self.base_panel.show_macro_shortcut(value) def _websocket_callback(self, action, data): if self.connecting is True: return if action == "notify_klippy_disconnected": logging.debug("Received notify_klippy_disconnected") self.printer.change_state("disconnected") return elif action == "notify_klippy_ready": self.printer.change_state("ready") elif action == "notify_status_update" and self.printer.get_state( ) != "shutdown": self.printer.process_update(data) elif action == "notify_filelist_changed": logging.debug("Filelist changed: %s", json.dumps(data, indent=2)) if self.files is not None: self.files.process_update(data) elif action == "notify_metadata_update": self.files.request_metadata(data['filename']) elif action == "notify_update_response": logging.info(f"{action}: {data}") elif action == "notify_power_changed": logging.debug("Power status changed: %s", data) self.printer.process_power_update(data) self.panels['splash_screen'].check_power_status() elif self.printer.get_state() not in [ "error", "shutdown" ] and action == "notify_gcode_response": if "Klipper state: Shutdown" in data: logging.debug( "Shutdown in gcode response, changing state to shutdown") self.printer.change_state("shutdown") if not (data.startswith("B:") and re.search( r'B:[0-9\.]+\s/[0-9\.]+\sT[0-9]+:[0-9\.]+', data)): if data.startswith("echo: "): self.show_popup_message(data[6:], 1) elif data.startswith("!! "): self.show_popup_message(data[3:], 3) logging.debug(json.dumps([action, data], indent=2)) if "SAVE_CONFIG" in data and self.printer.get_state( ) == "ready": script = {"script": "SAVE_CONFIG"} self._confirm_send_action( None, _("Save configuration?") + "\n\n" + _("Klipper will reboot"), "printer.gcode.script", script) self.base_panel.process_update(action, data) if self._cur_panels[-1] in self.subscriptions: self.panels[self._cur_panels[-1]].process_update(action, data) def _confirm_send_action(self, widget, text, method, params=None): if params is None: params = {} buttons = [{ "name": _("Continue"), "response": Gtk.ResponseType.OK }, { "name": _("Cancel"), "response": Gtk.ResponseType.CANCEL }] try: env = Environment(extensions=["jinja2.ext.i18n"], autoescape=True) env.install_gettext_translations(self.lang) j2_temp = env.from_string(text) text = j2_temp.render() except Exception as e: logging.debug(f"Error parsing jinja for confirm_send_action\n{e}") label = Gtk.Label() label.set_markup(text) label.set_hexpand(True) label.set_halign(Gtk.Align.CENTER) label.set_vexpand(True) label.set_valign(Gtk.Align.CENTER) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) if self.confirm is not None: self.confirm.destroy() self.confirm = self.gtk.Dialog(self, buttons, label, self._confirm_send_action_response, method, params) def _confirm_send_action_response(self, widget, response_id, method, params): if response_id == Gtk.ResponseType.OK: self._send_action(widget, method, params) widget.destroy() def _send_action(self, widget, method, params): self._ws.send_method(method, params) def printer_initializing(self, text=None, disconnect=False): self.close_popup_message() self.show_panel('splash_screen', "splash_screen", "Splash Screen", 2) if disconnect is True and self.printer is not None: self.shutdown = True self.printer.state = "disconnected" if text is not None: self.panels['splash_screen'].update_text(text) def search_power_devices(self, power_devices): if self.connected_printer is None: return found_devices = [] devices = self.printer.get_power_devices() logging.debug("Power devices: %s", devices) if devices is not None: for device in devices: for power_device in power_devices: if device == power_device and power_device not in found_devices: found_devices.append(power_device) if found_devices: logging.info("Found %s", found_devices) return found_devices else: logging.info("Power devices not found") return None def power_on(self, widget, devices): for device in devices: if self.printer.get_power_device_status(device) == "off": self.show_popup_message(_("Sending Power ON signal to: %s") % devices, level=1) logging.info("%s is OFF, Sending Power ON signal", device) self._ws.klippy.power_device_on(device) elif self.printer.get_power_device_status(device) == "on": logging.info("%s is ON", device) def init_printer(self): state = self.apiclient.get_server_info() if state is False: return False # Moonraker is ready, set a loop to init the printer self.reinit_count += 1 self.init_printer_timeout = GLib.timeout_add_seconds( 7, self.init_printer) self.shutdown = False powerdevs = self.apiclient.send_request("machine/device_power/devices") if powerdevs is not False: self.printer.configure_power_devices(powerdevs['result']) if state['result']['klippy_connected'] is False: self.panels['splash_screen'].update_text( _("Moonraker: connected") + f"\n\nKlipper: {state['result']['klippy_state']}\n\n" + _("Retry #%s") % self.reinit_count) return False printer_info = self.apiclient.get_printer_info() if printer_info is False: return self._update_splash_screen( "Unable to get printer info from moonraker") data = self.apiclient.send_request( "printer/objects/query?" + "&".join(PRINTER_BASE_STATUS_OBJECTS)) if data is False: return self._update_splash_screen( "Error getting printer object data") config = self.apiclient.send_request( "printer/objects/query?configfile") if config is False: return self._update_splash_screen( "Error getting printer config data") # Reinitialize printer, in case the printer was shut down and anything has changed. self.printer.reinit(printer_info['result'], config['result']['status']) self.ws_subscribe() extra_items = (self.printer.get_tools() + self.printer.get_heaters() + self.printer.get_fans() + self.printer.get_filament_sensors() + self.printer.get_output_pins()) data = self.apiclient.send_request( "printer/objects/query?" + "&".join(PRINTER_BASE_STATUS_OBJECTS + extra_items)) if data is False: return self._update_splash_screen( "Error getting printer object data with extra items") tempstore = self.apiclient.send_request("server/temperature_store") if tempstore is not False: self.printer.init_temp_store(tempstore['result']) self.printer.process_update(data['result']['status']) self.files.initialize() self.files.refresh_files() logging.info("Printer initialized") GLib.source_remove(self.init_printer_timeout) self.reinit_count = 0 return False def _update_splash_screen(self, msg): logging.info(msg) self.panels['splash_screen'].update_text(msg) return False def printer_ready(self): self.close_popup_message() # Force an update to printer webhooks state in case the update is missed due to websocket subscribe not yet sent self.printer.process_update({ "webhooks": { "state": "ready", "state_message": "Printer is ready" } }) self.show_panel('main_panel', "main_menu", _("Home"), 2, items=self._config.get_menu_items("__main"), extrudercount=self.printer.get_extruder_count()) self.ws_subscribe() if "job_status" in self.panels: self.remove_subscription("job_status") del self.panels["job_status"] def printer_printing(self): self.close_popup_message() self.show_panel('job_status', "job_status", "Print Status", 2) self.base_panel.show_heaters(True) self.base_panel.show_macro_shortcut( self._config.get_main_config().getboolean('side_macro_shortcut')) def show_keyboard(self, widget=None, event=None, entry=None): if self.keyboard is not None: return box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box.set_size_request(self.gtk.get_content_width(), self.gtk.get_keyboard_height()) if self._config.get_main_config().getboolean("use-matchbox-keyboard", False): env = os.environ.copy() usrkbd = os.path.expanduser("~/.matchbox/keyboard.xml") if os.path.isfile(usrkbd): env["MB_KBD_CONFIG"] = usrkbd else: env["MB_KBD_CONFIG"] = "ks_includes/locales/keyboard.xml" p = subprocess.Popen(["matchbox-keyboard", "--xid"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) xid = int(p.stdout.readline()) logging.debug(f"XID {xid}") logging.debug(f"PID {p.pid}") keyboard = Gtk.Socket() box.get_style_context().add_class("keyboard_matchbox") box.pack_start(keyboard, True, True, 0) self.base_panel.get_content().pack_end(box, False, False, 0) self.show_all() keyboard.add_id(xid) self.keyboard = {"box": box, "process": p, "socket": keyboard} return if entry is None: logging.debug("Error: no entry provided for keyboard") return box.get_style_context().add_class("keyboard_box") box.add(Keyboard(self, self.remove_keyboard, entry=entry)) self.keyboard = {"entry": entry, "box": box} self.base_panel.get_content().pack_end(box, False, False, 0) self.base_panel.get_content().show_all() def remove_keyboard(self, widget=None, event=None): if self.keyboard is None: return if 'process' in self.keyboard: os.kill(self.keyboard['process'].pid, signal.SIGTERM) self.base_panel.get_content().remove(self.keyboard['box']) self.keyboard = None def change_cursor(self, cursortype=None): if cursortype == "watch": os.system("xsetroot -cursor_name watch") elif self.show_cursor: self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) os.system("xsetroot -cursor_name arrow") else: self.get_window().set_cursor( Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR)) os.system( "xsetroot -cursor ks_includes/emptyCursor.xbm ks_includes/emptyCursor.xbm" ) return
def connect_printer(self, name): data = { "moonraker_host": "127.0.0.1", "moonraker_port": "7125", "moonraker_api_key": False } self.connecting_to_printer = name if self.connected_printer == name: if self.printer_select_prepanel is not None: self.show_panel(self.printer_select_prepanel, "", "", 2) self.printer_select_prepanel = None while len(self.printer_select_callbacks) > 0: i = self.printer_select_callbacks.pop(0) i() if self.printer.get_state() not in [ "disconnected", "error", "startup", "shutdown" ]: self.base_panel.show_heaters(True) self.base_panel.show_printer_select(True) self.base_panel.show_macro_shortcut( self._config.get_main_config().getboolean( 'side_macro_shortcut')) return # Cleanup self.printer_select_callbacks = [] self.printer_select_prepanel = None if self.files is not None: self.files.reset() self.files = None if self.printer is not None: self.printer.reset() self.printer = None for printer in self._config.get_printers(): pname = list(printer)[0] if pname != name: continue data = printer[pname] break if self._ws is not None: self._ws.close() self.connecting = True logging.info(f"Connecting to printer: {name}") self.apiclient = KlippyRest(data["moonraker_host"], data["moonraker_port"], data["moonraker_api_key"]) self.printer = Printer({"software_version": "Unknown"}, { 'configfile': { 'config': {} }, 'print_stats': { 'state': 'disconnected' }, 'virtual_sdcard': { 'is_active': False } }, self.state_execute) self._remove_all_panels() self.subscriptions = [] for panel in list(self.panels): if panel not in ["printer_select", "splash_screen"]: del self.panels[panel] for dialog in self.dialogs: dialog.destroy() self.base_panel.show_printer_select(True) self.printer_initializing(_("Connecting to %s") % name) self.printer.set_callbacks({ "disconnected": self.state_disconnected, "error": self.state_error, "paused": self.state_paused, "printing": self.state_printing, "ready": self.state_ready, "startup": self.state_startup, "shutdown": self.state_shutdown }) self._ws = KlippyWebsocket( self, { "on_connect": self.init_printer, "on_message": self._websocket_callback, "on_close": self.printer_initializing }, data["moonraker_host"], data["moonraker_port"]) self.files = KlippyFiles(self) self._ws.initial_connect() self.connecting = False self.connected_printer = name logging.debug(f"Connected to printer: {name}")