diff --git a/config/defaults.conf b/config/defaults.conf index afc07b4bc..9b7d7c49d 100644 --- a/config/defaults.conf +++ b/config/defaults.conf @@ -6,22 +6,22 @@ [preheat PLA] bed = 60 -extruder = 215 +extruder = 160 extruder1 = 215 [preheat TPU] bed = 65 -extruder = 225 +extruder = 160 extruder1 = 225 [preheat PETG] bed = 80 -extruder = 235 +extruder = 160 extruder1 = 235 [preheat ABS] bed = 110 -extruder = 245 +extruder = 160 extruder1 = 245 [include main_menu.conf] diff --git a/config/main_menu.conf b/config/main_menu.conf index cc5992644..d016b0bee 100644 --- a/config/main_menu.conf +++ b/config/main_menu.conf @@ -106,11 +106,11 @@ name: {{ gettext('Update') }} icon: refresh panel: updater -# [menu __main more input_shaper] -# name: {{ gettext('Input Shaper') }} -# icon: move -# panel: input_shaper -# enable: {{ 'input_shaper' in printer.config_sections }} +[menu __main more input_shaper] +name: {{ gettext('Input Shaper') }} +icon: move +panel: input_shaper +enable: {{ 'mcu adxl' in printer.config_sections }} # [menu __main more save] # name: {{ gettext('Save Config') }} diff --git a/ks_includes/config.py b/ks_includes/config.py index 7703ccefc..0d7d670be 100644 --- a/ks_includes/config.py +++ b/ks_includes/config.py @@ -162,13 +162,15 @@ def validate_config(self, config, string="", remove=False): bools = ( 'invert_x', 'invert_y', 'invert_z', '24htime', 'only_heaters', 'show_cursor', 'confirm_estop', 'autoclose_popups', 'use_dpms', 'use_default_menu', 'side_macro_shortcut', 'use-matchbox-keyboard', - 'show_heater_power', 'show_lock_button', "show_scroll_steppers", "auto_open_extrude" + 'show_heater_power', 'show_lock_button', "show_scroll_steppers", "auto_open_extrude", + 'ap_mode_enabled', 'show_ap_toggle' ) strs = ( 'default_printer', 'language', 'print_sort_dir', 'theme', 'screen_blanking_printing', 'font_size', 'print_estimate_method', 'screen_blanking', "screen_on_devices", "screen_off_devices", 'print_view', - "lock_password" + "lock_password", 'ap_ssid', 'ap_password' ) + # Note: ap_mode_enabled is handled as bool but not in configurable_options numbers = ( 'job_complete_timeout', 'job_error_timeout', 'move_speed_xy', 'move_speed_z', 'print_estimate_compensation', 'width', 'height', @@ -316,6 +318,14 @@ def _create_configurable_options(self, screen): {"show_cursor": {"section": "main", "name": _("Show cursor"), "type": "binary", "tooltip": _("For mouse control or to verify touchscreen accuracy"), "value": "False", "callback": screen.update_cursor}}, + {"show_ap_toggle": { + "section": "main", + "name": _("Show Access Point toggle"), + "type": "binary", + "tooltip": _("Test feature, unstable behavior possible"), + "value": "False", + "callback": screen.reload_panels + }}, # {"": {"section": "main", "name": _(""), "type": ""}} ] diff --git a/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.mo b/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.mo index 82267de0e..a487528bd 100644 Binary files a/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.mo and b/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.mo differ diff --git a/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.po b/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.po index 1128b1f47..037db9860 100644 --- a/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.po +++ b/ks_includes/locales/ru/LC_MESSAGES/KlipperScreen.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: KlipperScreen\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-11-26 12:31-0300\n" -"PO-Revision-Date: 2025-12-25 18:27+0300\n" +"PO-Revision-Date: 2026-01-28 17:01+0300\n" "Last-Translator: gfbdrgng \n" "Language-Team: Russian \n" "Language: ru\n" @@ -237,6 +237,9 @@ msgstr "Прошло:" msgid "Emergency Stop" msgstr "Аварийная остановка" +msgid "On/Off auto power off" +msgstr "Вкл/Выкл автовыключение" + msgid "Enable screen power management" msgstr "Включить панель управление питанием" @@ -938,6 +941,12 @@ msgstr "Обновление" msgid "Show cursor" msgstr "Отображать курсор" +msgid "Show Access Point toggle" +msgstr "Показывать переключатель точки доступа" + +msgid "Test feature, unstable behavior possible" +msgstr "Тестовая функция, возможны сбои" + msgid "For mouse control or to verify touchscreen accuracy" msgstr "Для управления мышью или для проверки точности сенсора" diff --git a/ks_includes/sdbus_nm.py b/ks_includes/sdbus_nm.py index e0e148b4b..eb965119d 100644 --- a/ks_includes/sdbus_nm.py +++ b/ks_includes/sdbus_nm.py @@ -2,7 +2,9 @@ # TODO device selection/swtichability # Alfredo Monclus (alfrix) 2024 import logging +import re import subprocess +import socket from uuid import uuid4 import sdbus @@ -165,10 +167,15 @@ def is_known(self, ssid): def get_ip_address(self): active_connection_path = self.nm.primary_connection if not active_connection_path or active_connection_path == "/": - return "?" - active_connection = ActiveConnection(active_connection_path) - ip_info = IPv4Config(active_connection.ip4_config) - return ip_info.address_data[0]["address"][1] + # Try to get IP address directly from interface + return self.get_interface_ip_address() + try: + active_connection = ActiveConnection(active_connection_path) + ip_info = IPv4Config(active_connection.ip4_config) + return ip_info.address_data[0]["address"][1] + except Exception as e: + logging.debug(f"Failed to get IP from active connection: {e}") + return self.get_interface_ip_address() def get_networks(self): networks = [] @@ -395,3 +402,139 @@ def monitor_connection_status(self): def enable_monitoring(self, enable): self.monitor_connection = enable + + def get_interface_ip_address(self): + """Get IP address directly from network interface""" + try: + interface = self.get_primary_interface() + if not interface: + return "?" + # Try using socket + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + pass + # Fallback to ip command + result = subprocess.run( + ["ip", "-4", "addr", "show", interface], + capture_output=True, + text=True, + timeout=2 + ) + if result.returncode == 0: + match = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', result.stdout) + if match: + return match.group(1) + except Exception as e: + logging.debug(f"Failed to get interface IP: {e}") + return "?" + + def is_access_point_mode(self): + """Check if wireless device is in AP mode""" + if not self.wlan_device: + return False + try: + # Check active connection type + active_connection_path = self.wlan_device.active_connection + if active_connection_path and active_connection_path != "/": + active_connection = ActiveConnection(active_connection_path) + connection_path = active_connection.connection + if connection_path and connection_path != "/": + connection_settings = NetworkConnectionSettings(connection_path) + settings = connection_settings.get_settings() + if "802-11-wireless" in settings: + mode = settings["802-11-wireless"].get("mode", [None, None])[1] + return mode == "ap" + except Exception as e: + logging.debug(f"Failed to check AP mode: {e}") + return False + + def get_access_point_connection_path(self): + """Get connection path for access point if exists""" + try: + saved_network_paths = NetworkManagerSettings().list_connections() + for netpath in saved_network_paths: + saved_con = NetworkConnectionSettings(netpath) + con_settings = saved_con.get_settings() + if (con_settings["connection"]["type"][1] == "802-11-wireless" and + con_settings.get("802-11-wireless", {}).get("mode", [None, None])[1] == "ap"): + return netpath + except Exception as e: + logging.debug(f"Failed to get AP connection path: {e}") + return None + + def create_access_point(self, ssid, password): + """Create and activate access point""" + try: + # Delete existing AP connection if exists + ap_path = self.get_access_point_connection_path() + if ap_path: + NetworkConnectionSettings(ap_path).delete() + + # Disconnect current connection + if self.wlan_device.active_connection and self.wlan_device.active_connection != "/": + self.wlan_device.disconnect() + + properties: NetworkManagerConnectionProperties = { + "connection": { + "id": ("s", "KlipperScreen-AP"), + "uuid": ("s", str(uuid4())), + "type": ("s", "802-11-wireless"), + "interface-name": ("s", self.wlan_device.interface), + "autoconnect": ("b", True), + }, + "802-11-wireless": { + "mode": ("s", "ap"), + "ssid": ("ay", ssid.encode("utf-8")), + "security": ("s", "802-11-wireless-security"), + }, + "802-11-wireless-security": { + "key-mgmt": ("s", "wpa-psk"), + "psk": ("s", password), + }, + "ipv4": { + "method": ("s", "shared"), + }, + "ipv6": { + "method": ("s", "ignore"), + }, + } + + connection_path = NetworkManagerSettings().add_connection(properties) + logging.info(f"Created AP connection: {connection_path}") + + # Activate the connection + self.popup(f"{ssid}\nStarting Access Point", 1) + self.nm.activate_connection(connection_path) + return {"status": "success"} + except exceptions.NmSettingsPermissionDeniedError: + logging.exception("Insufficient privileges") + return { + "error": "insufficient_privileges", + "message": "Insufficient privileges", + } + except Exception as e: + logging.exception("Couldn't create access point") + return {"error": "unknown", "message": "Couldn't create access point" + f"\n{e}"} + + def remove_access_point(self): + """Remove access point and return to normal mode""" + try: + # Disconnect current connection + if self.wlan_device.active_connection and self.wlan_device.active_connection != "/": + self.wlan_device.disconnect() + + # Delete AP connection + ap_path = self.get_access_point_connection_path() + if ap_path: + NetworkConnectionSettings(ap_path).delete() + logging.info("Removed AP connection") + return {"status": "success"} + except Exception as e: + logging.exception("Couldn't remove access point") + return {"error": "unknown", "message": "Couldn't remove access point" + f"\n{e}"} + return {"status": "success"} diff --git a/panels/network.py b/panels/network.py index e57190d94..a3238a578 100644 --- a/panels/network.py +++ b/panels/network.py @@ -93,9 +93,49 @@ def __init__(self, screen, title): scroll = self._gtk.ScrolledWindow() self.labels['main_box'] = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, vexpand=True) + # AP test feature visibility flag + self.show_ap_toggle = self._config.get_main_config().getboolean('show_ap_toggle', False) + + # AP-related attributes (used only if show_ap_toggle is True) + self.ap_ssid = self._config.get_main_config().get('ap_ssid', 'zboltprinter') + self.ap_password = self._config.get_main_config().get('ap_password', 'zboltprinter') + self.is_ap_mode = False + if self.sdbus_nm.wifi: self.labels['main_box'].pack_start(sbox, False, False, 5) - GLib.idle_add(self.load_networks) + if self.show_ap_toggle: + # Initialize AP toggle switch (test feature) + ap_mode_enabled = self._config.get_main_config().getboolean('ap_mode_enabled', False) + current_ap_mode = self.sdbus_nm.is_access_point_mode() + + self.ap_toggle = Gtk.Switch( + width_request=round(self._gtk.font_size * 2), + height_request=round(self._gtk.font_size), + active=current_ap_mode or ap_mode_enabled + ) + self.ap_toggle.connect("notify::active", self.toggle_ap_mode) + self.ap_label = Gtk.Label(label=_("AP"), hexpand=False) + + ap_container = Gtk.Box(spacing=5, hexpand=False) + ap_container.add(self.ap_label) + ap_container.add(self.ap_toggle) + + # Insert AP container at the beginning of sbox + sbox.pack_start(ap_container, False, False, 5) + + # Check initial AP mode state and restore if needed + if self.sdbus_nm.is_access_point_mode(): + self.is_ap_mode = True + self.ap_toggle.set_active(True) + GLib.idle_add(self.update_ap_display) + elif ap_mode_enabled and not current_ap_mode: + logging.info("Restoring AP mode from saved configuration") + GLib.idle_add(self.restore_ap_mode) + else: + GLib.idle_add(self.load_networks) + else: + # AP toggle hidden: always use normal network list + GLib.idle_add(self.load_networks) scroll.add(self.network_list) self.sdbus_nm.enable_monitoring(True) self.conn_status = GLib.timeout_add_seconds(1, self.sdbus_nm.monitor_connection_status) @@ -355,7 +395,21 @@ def show_add_network(self, widget, ssid): def update_all_networks(self): self.interface = self.sdbus_nm.get_primary_interface() self.labels['interface'].set_text(_("Interface") + f': {self.interface}') - self.labels['ip'].set_text(f"IP: {self.sdbus_nm.get_ip_address()}") + self.update_ip_display() + + # Check if AP mode changed externally + ap_mode = self.sdbus_nm.is_access_point_mode() + if ap_mode != self.is_ap_mode: + self.is_ap_mode = ap_mode + self.ap_toggle.set_active(ap_mode) + if ap_mode: + self.update_ap_display() + return True + + # If in AP mode, don't update network list + if self.is_ap_mode: + return True + nets = self.sdbus_nm.get_networks() remove = [bssid for bssid in self.network_rows.keys() if bssid not in [net['BSSID'] for net in nets]] for bssid in remove: @@ -411,6 +465,8 @@ def update_single_network_info(self): return True def reload_networks(self, widget=None): + if self.is_ap_mode: + return self.deactivate() del self.network_rows self.network_rows = {} @@ -428,12 +484,17 @@ def activate(self): return if self.update_timeout is None: if self.sdbus_nm.wifi: - if self.reload_button.get_sensitive(): - self._gtk.Button_busy(self.reload_button, True) - self.sdbus_nm.rescan() - self.load_networks() - self.update_all_networks() - self.update_timeout = GLib.timeout_add_seconds(5, self.update_all_networks) + if self.is_ap_mode: + # In AP mode, just update IP + self.update_ip_display() + self.update_timeout = GLib.timeout_add_seconds(5, self.update_ip_display) + else: + if self.reload_button.get_sensitive(): + self._gtk.Button_busy(self.reload_button, True) + self.sdbus_nm.rescan() + self.load_networks() + self.update_all_networks() + self.update_timeout = GLib.timeout_add_seconds(5, self.update_all_networks) else: self.update_single_network_info() self.update_timeout = GLib.timeout_add_seconds(5, self.update_single_network_info) @@ -450,6 +511,10 @@ def deactivate(self): def toggle_wifi(self, switch, gparams): enable = switch.get_active() logging.info(f"WiFi {enable}") + if not enable and self.sdbus_nm.is_access_point_mode(): + # If disabling WiFi and AP is active, disable AP first + self.ap_toggle.set_active(False) + self.toggle_ap_mode(self.ap_toggle, None) self.sdbus_nm.toggle_wifi(enable) if enable: self.reload_button.show() @@ -457,6 +522,108 @@ def toggle_wifi(self, switch, gparams): else: self.reload_button.hide() + def restore_ap_mode(self): + """Restore AP mode from saved configuration""" + if not self.sdbus_nm.is_wifi_enabled(): + logging.warning("Cannot restore AP mode: WiFi is disabled") + return False + + result = self.sdbus_nm.create_access_point(self.ap_ssid, self.ap_password) + if "error" in result: + logging.error(f"Failed to restore AP mode: {result['message']}") + self.ap_toggle.set_active(False) + # Clear saved state if restoration failed + if 'main' not in self._config.get_config().sections(): + self._config.get_config().add_section('main') + self._config.set('main', 'ap_mode_enabled', 'False') + self._config.save_user_config_options() + # Load networks if AP restoration failed + GLib.idle_add(self.load_networks) + return False + + self.is_ap_mode = True + self.ap_toggle.set_active(True) + # Update display after restoring AP + self.update_ap_display() + self.update_ip_display() + return False # Return False to prevent being called again by GLib.idle_add + + def toggle_ap_mode(self, switch, gparams): + enable = switch.get_active() + logging.info(f"AP mode {enable}") + + if not self.sdbus_nm.is_wifi_enabled(): + switch.set_active(False) + self._screen.show_popup_message(_("WiFi must be enabled first"), level=2) + return + + # Save state to configuration + if 'main' not in self._config.get_config().sections(): + self._config.get_config().add_section('main') + self._config.set('main', 'ap_mode_enabled', 'True' if enable else 'False') + self._config.save_user_config_options() + + if enable: + # Enable AP mode + result = self.sdbus_nm.create_access_point(self.ap_ssid, self.ap_password) + if "error" in result: + switch.set_active(False) + self._config.set('main', 'ap_mode_enabled', 'False') + self._config.save_user_config_options() + self._screen.show_popup_message(result["message"], level=2) + return + self.is_ap_mode = True + # Hide network list and show only AP info + self.update_ap_display() + else: + # Disable AP mode + result = self.sdbus_nm.remove_access_point() + if "error" in result: + self._screen.show_popup_message(result["message"], level=2) + self.is_ap_mode = False + # Restore normal network list + self.reload_networks() + + # Update IP display + self.update_ip_display() + + def update_ap_display(self): + """Update display to show only AP information""" + # Clear network list + for child in list(self.network_list.get_children()): + self.network_list.remove(child) + self.network_rows.clear() + self.networks.clear() + + # Add AP info display + ap_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10, + valign=Gtk.Align.CENTER, vexpand=True) + ap_info_box.get_style_context().add_class("frame-item") + + ap_name_label = Gtk.Label() + ap_name_label.set_markup(f"{self.ap_ssid}") + ap_name_label.set_halign(Gtk.Align.CENTER) + + ap_status_label = Gtk.Label(label=_("Access Point Mode")) + ap_status_label.set_halign(Gtk.Align.CENTER) + + ap_password_label = Gtk.Label() + ap_password_label.set_markup(f"{_('Password')}: {self.ap_password}") + ap_password_label.set_halign(Gtk.Align.CENTER) + + ap_info_box.add(ap_name_label) + ap_info_box.add(ap_status_label) + ap_info_box.add(ap_password_label) + + self.network_list.add(ap_info_box) + self.network_list.show_all() + + def update_ip_display(self): + """Update IP address display""" + ip = self.sdbus_nm.get_ip_address() + self.labels['ip'].set_text(f"IP: {ip}") + return True + def show_fullscreen_qrcode(self, widget): curr_ip = self.sdbus_nm.get_ip_address() diff --git a/panels/shutdown.py b/panels/shutdown.py index b37d6dc52..57642b552 100644 --- a/panels/shutdown.py +++ b/panels/shutdown.py @@ -13,8 +13,17 @@ def __init__(self, screen, title): title = title or _("Shutdown") super().__init__(screen, title) + self.has_change_poweroff = False + if self._printer: + macros = self._printer.get_config_section_list("gcode_macro ") + self.has_change_poweroff = any("CHANGE_POWEROFF" in macro.upper() for macro in macros) + estop = self._gtk.Button("emergency", _("Emergency Stop"), "color2") estop.connect("clicked", self.emergency_stop) + auto_poweroff = self._gtk.Button("shutdown", _("On/Off auto power off"), "color2") + auto_poweroff.connect("clicked", self.toggle_auto_poweroff) + + safety_button = auto_poweroff if self.has_change_poweroff else estop poweroff = self._gtk.Button("shutdown", _("Shutdown"), "color1") poweroff.connect("clicked", self.reboot_poweroff, "shutdown") @@ -33,7 +42,7 @@ def __init__(self, screen, title): self.main = Gtk.Grid(row_homogeneous=True, column_homogeneous=True) if self._show_lock_button: if self._printer and self._printer.state not in {'disconnected', 'startup', 'shutdown', 'error'}: - self.main.attach(estop, 1, 0, 1, 1) + self.main.attach(safety_button, 1, 0, 1, 1) self.main.attach(restart_ks, 2, 0, 1, 1) self.main.attach(lock_screen, 0, 0, 1, 2) self.main.attach(poweroff, 1, 1, 1, 1) @@ -41,7 +50,7 @@ def __init__(self, screen, title): self.content.add(self.main) else: if self._printer and self._printer.state not in {'disconnected', 'startup', 'shutdown', 'error'}: - self.main.attach(estop, 0, 0, 1, 1) + self.main.attach(safety_button, 0, 0, 1, 1) self.main.attach(restart_ks, 1, 0, 1, 1) self.main.attach(poweroff, 0, 1, 1, 1) self.main.attach(restart, 1, 1, 1, 1) @@ -99,3 +108,7 @@ def turn_off_power_devices(self): if power_devices and self._printer.get_power_devices(): logging.info(f"Turning off associated power devices: {power_devices}") self._screen.power_devices(widget=None, devices=power_devices, on=False) + + def toggle_auto_poweroff(self, widget): + if self._screen and self._screen._ws and self._screen._ws.connected: + self._screen._send_action(widget, "printer.gcode.script", {"script": "CHANGE_POWEROFF"})