Skip to content
2 changes: 2 additions & 0 deletions BlocksScreen/devices/amu/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class MMUState:
filament: str # "Loaded" | "Unloaded" | "Unknown"
filament_pos: FilamentPos
action: str
bowden_progress: float
print_state: str
reason_for_pause: str
has_bypass: bool
Expand Down Expand Up @@ -142,6 +143,7 @@ def from_status(cls, data: dict) -> "MMUState":
filament=data.get("filament", "Unknown"),
filament_pos=FilamentPos(data.get("filament_pos", FilamentPos.UNKNOWN)),
action=data.get("action", ""),
bowden_progress=data.get("bowden_progress", -1),
print_state=data.get("print_state", ""),
reason_for_pause=data.get("reason_for_pause", ""),
has_bypass=data.get("has_bypass", False),
Expand Down
116 changes: 100 additions & 16 deletions BlocksScreen/lib/panels/filamentTab.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
from collections import deque
from typing import Deque

from devices.amu import AMUManager
from devices.amu.models import GateStatus
Expand All @@ -20,6 +19,7 @@
from lib.utils.blocks_linedit import BlocksCustomLinEdit
from lib.utils.icon_button import IconButton
from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem
from lib.utils.toolmap import MmuToolmapWidget
from PyQt6 import QtCore, QtGui, QtWidgets

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -52,15 +52,20 @@ def __init__(

self._previous_gate_states: dict[int, bool] = {}
self.pre_gate_idx = {}
self.popup_gates: Deque = deque()
self.popup_gates: deque = deque()
self._spool_id_map: dict[str, dict] = {}
self._current_field: QtWidgets.QLineEdit | None = None
self._color_target_field = None
self._material_filter: str | None = None
self.moonraker_run = True

self.amu_manager.mmu_state_changed.connect(self.on_mmu_state_changed)
self._setup_pre_gate_popup()
self._setup_load_popup()
self.amu_manager.mmu_state_changed.connect(self.on_mmu_state_changed)

self._extruder_current_temp = 0.0
self._extruder_target_temp = 0.0
self.printer.extruder_update.connect(self.on_extruder_update)

self.spoolmanPanel = SpoolmanPage(self)
self.addWidget(self.spoolmanPanel)
Expand Down Expand Up @@ -98,7 +103,9 @@ def __init__(
)
)

self._basic_panel = BasicFilamentPanel(self.printer, self.cfg, parent=self)
self._basic_panel = BasicFilamentPanel(
self.printer, self.cfg, parent=self, load_popup=self.load_popup
)
self._basic_panel.run_gcode.connect(self.run_gcode)
self._basic_panel.call_load_panel.connect(self.call_load_panel)
self._basic_panel.request_back.connect(self.request_back)
Expand All @@ -116,7 +123,17 @@ def __init__(

self.run_gcode.connect(self.ws.api.run_gcode)

def in_case_error(self):
"""Reset the popup and spool info in case of an error."""
self._reset_popup()
self.reset_spool_info()
self._add_spool_page.setFilter(None)
self._add_filament_page.setData("---", 0)
self.load_state = False
self.load_popup.hide()

def handle_moonraker_components(self):
"""Build the pre-gate popup pages once, choosing spoolman vs. manual-entry order."""
if self.moonraker_run:
components = self.ws._moonRest.get_server_info()
if "spoolman" not in components["result"].get("components", []):
Expand All @@ -138,6 +155,43 @@ def change_page(self, index: int) -> None:
"""
self.request_change_page.emit(1, index)

def _setup_load_popup(self) -> None:
self.load_popup = BasePopup(self, floating=False, dialog=False)
load_container = QtWidgets.QWidget(self.load_popup)
load_layout = QtWidgets.QVBoxLayout(load_container)

self.load_status_label = QtWidgets.QLabel(load_container)
self.load_status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
label_font = QtGui.QFont()
label_font.setPointSize(20)
self.load_status_label.setFont(label_font)
self.load_status_label.setStyleSheet("color: #ffffff; background: transparent;")
self.load_status_label.setMaximumHeight(100)
load_layout.addWidget(self.load_status_label)

self.load_status_widget = MmuToolmapWidget(load_container)
self.load_status_widget.set_left_text("Auxiliary Extruder")
load_layout.addWidget(self.load_status_widget)

self.load_popup.add_widget(load_container)

@QtCore.pyqtSlot(str, str, float, name="on-extruder-update")
def on_extruder_update(
self, extruder_name: str, field: str, new_value: float
) -> None:
"""Track the extruder's current/target temp and feed it to the load status widget."""
if extruder_name != "extruder":
return
if field == "temperature":
self._extruder_current_temp = new_value
elif field == "target":
self._extruder_target_temp = new_value
else:
return
self.load_status_widget.set_temps(
self._extruder_current_temp, self._extruder_target_temp
)

def _setup_pre_gate_popup(self) -> None:
self._numpad = CustomNumpad(self)
self._numpad.hide()
Expand Down Expand Up @@ -512,6 +566,7 @@ def _add_row(title_text: str) -> QtWidgets.QLabel:
return page

def handle_skip_button(self):
"""Map the pending pre-gate gate to an empty spool and dismiss the popup."""
gate = self.pre_gate_idx.get("gate", 0)
self._reset_popup()
self.run_gcode.emit(
Expand All @@ -520,7 +575,7 @@ def handle_skip_button(self):
if self._popup_callback is not None:
try:
self._popup_callback()
except Exception as e:
except Exception as e: # noqa: BLE001 - arbitrary caller-supplied callback
logger.error(f"Error executing pre-gate accept callback: {e}")
finally:
self._popup_callback = None
Expand Down Expand Up @@ -563,6 +618,7 @@ def handle_popup(self, force=False):

@QtCore.pyqtSlot(int, str, str, "PyQt_PyObject", name="open-pregate-popup")
def open_pregate_popup(self, temp, material, name, callback=None):
"""Open the pre-gate popup pre-filled with a detected filament's info and *callback*."""
self._popup_name.setText(name)
self._popup_material.setText(material)
self._popup_temp.setText(str(temp))
Expand Down Expand Up @@ -601,7 +657,7 @@ def on_popup_accept(self):
if self._popup_callback is not None:
try:
self._popup_callback()
except Exception as e:
except Exception as e: # noqa: BLE001 - arbitrary caller-supplied callback
logger.error(f"Error executing pre-gate accept callback: {e}")
finally:
self._popup_callback = None
Expand Down Expand Up @@ -716,14 +772,15 @@ def _on_spool_selected(self) -> None:
if self._popup_callback is not None:
try:
self._popup_callback()
except Exception as e:
except Exception as e: # noqa: BLE001 - arbitrary caller-supplied callback
logger.error(f"Error executing pre-gate accept callback: {e}")
finally:
self._popup_callback = None

self.handle_popup()

def reset_spool_info(self):
"""Clear the selected-spool detail labels back to their placeholder state."""
self.filament_name_label.setText("N/A")
self.material_label.setText("N/A")
self.weight_label.setText("N/A")
Expand Down Expand Up @@ -855,7 +912,10 @@ def on_mmu_state_changed(self, mmu_state):

if not self.amu_configured:
if len(mmu_state.gates) > 1:
self.amupage = AMUpage(self.amu_manager, parent=self)
self.load_status_widget.set_left_text("AMU")
self.amupage = AMUpage(
self.amu_manager, parent=self, load_popup=self.load_popup
)
self.addWidget(self.amupage)
try:
self.removeWidget(self._basic_panel)
Expand All @@ -881,26 +941,50 @@ def on_mmu_state_changed(self, mmu_state):
self.printer.print_stats_update[str, float].connect(
self.amupage.on_print_stats_update
)
self.amupage.call_load_panel.connect(self.call_load_panel)
self.amupage.request_keyboard.connect(self._on_show_keyboard)
self.amupage.request_color_wheel.connect(self._open_color_wheel)

else:
self.load_status_widget.set_left_text("Auxiliary Extruder")
self.amu_configured = True

if self.load_state:
if mmu_state.action == "Idle":
self.load_state = False
self.call_load_panel.emit(False, "", True)
self.load_popup.hide()
if not len(mmu_state.gates) > 1:
self._basic_panel.change_page(0)
return
self.call_load_panel.emit(True, mmu_state.action, True)

if mmu_state.action == "Loading" or mmu_state.action == "Unloading":
elif mmu_state.action in ("Loading", "Unloading"):
self.load_state = True
self.call_load_panel.emit(True, mmu_state.action, True)
self.load_popup.show()

if not self.load_popup.isVisible():
return
self.load_status_widget.set_filament_pos(
mmu_state.filament_pos, mmu_state.bowden_progress
)

for sensor_name in ("mmu_pre_gate", "mmu_gate", "toolhead"):
self.load_status_widget.set_sensor(
sensor_name, bool(mmu_state.sensors.get(sensor_name))
)
self.load_status_widget.set_action(mmu_state.action)

gate_info = mmu_state.current_gate_info
raw_color = (
str(gate_info.color).lstrip("#")[:6]
if gate_info and gate_info.color
else ""
)
parsed_color = QtGui.QColor("#" + raw_color) if raw_color else QtGui.QColor()
if parsed_color.isValid():
self.load_status_widget.set_gate_color(parsed_color)
else:
self.load_status_widget.set_gate_color(QtGui.QColor("#ffffff"))

self.load_status_label.setText(mmu_state.action)

def setupUi(self):
"""Build the tab's landing page (title + Filament Control / Spoolman buttons)."""
self.resize(710, 410)
self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
widget = QtWidgets.QWidget()
Expand Down
1 change: 1 addition & 0 deletions BlocksScreen/lib/panels/mainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def __init__(self):
self.printPanel.request_back.connect(slot=self.global_back)
self.printPanel.on_cancel_print.connect(slot=self.on_cancel_print)
self.in_case_error.connect(self.printPanel.in_case_error)
self.in_case_error.connect(self.filamentPanel.in_case_error)

self.show_notifications.connect(self.notiPage.new_notication)

Expand Down
53 changes: 28 additions & 25 deletions BlocksScreen/lib/panels/widgets/amuPage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from devices.amu import AMUManager
from devices.amu.models import GateInfo
from lib.panels.widgets.amuWidgets import SpoolCarousel, SpoolInfoPanel
from lib.panels.widgets.basePopup import BasePopup
from lib.utils.blocks_frame import BlocksCustomFrame
from lib.utils.icon_button import IconButton
from PyQt6 import QtCore, QtGui, QtWidgets
Expand All @@ -29,12 +30,14 @@ class AMUpage(QtWidgets.QStackedWidget):
request_change_tab: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal(
int, name="request_change_tab"
)
call_load_panel = QtCore.pyqtSignal(bool, str, bool, name="call-load-panel")

def __init__(self, amu_manager, parent=None):
def __init__(
self, amu_manager, parent=None, *, load_popup: BasePopup | None = None
):
super().__init__(parent)
self.current_index = -1
self.amu_manager: AMUManager = amu_manager
self.load_popup = load_popup
self._build_ui()

self.main_back_button.clicked.connect(self.request_back)
Expand Down Expand Up @@ -84,13 +87,13 @@ def __init__(self, amu_manager, parent=None):
self.info_panel.loadRequested.connect(
lambda: {
self.amu_manager.load_gate(),
self.call_load_panel.emit(True, "Loading", True),
self.load_popup.show(),
}
)
self.info_panel.unloadRequested.connect(
lambda: {
self.amu_manager.unload(),
self.call_load_panel.emit(True, "Unloading", True),
self.load_popup.show(),
}
)
self.info_panel.ejectRequested.connect(self.amu_manager.eject_gate)
Expand All @@ -102,29 +105,28 @@ def __init__(self, amu_manager, parent=None):
@QtCore.pyqtSlot(str, float, name="on_print_stats_update")
@QtCore.pyqtSlot(str, str, name="on_print_stats_update")
def on_print_stats_update(self, field: str, value: dict | float | str) -> None:
if isinstance(value, str):
if "state" in field:
self.state = value
if value in ("printing", "pausing", "paused", "resuming"):
try:
self.main_back_button.clicked.disconnect()
except TypeError:
pass

self.main_back_button.clicked.connect(
lambda: self.request_change_tab.emit(0)
)

else:
try:
self.main_back_button.clicked.disconnect()
except TypeError:
pass
self.main_back_button.clicked.connect(
lambda: self.request_back.emit()
)
"""Rewire the back button between "request_back" and "change to tab 0" based on print state."""
if isinstance(value, str) and "state" in field:
self.state = value
if value in ("printing", "pausing", "paused", "resuming"):
try:
self.main_back_button.clicked.disconnect()
except TypeError:
pass

self.main_back_button.clicked.connect(
lambda: self.request_change_tab.emit(0)
)

else:
try:
self.main_back_button.clicked.disconnect()
except TypeError:
pass
self.main_back_button.clicked.connect(lambda: self.request_back.emit())

def on_mmu_state_changed(self, mmu_state):
"""Refresh the carousel and the info panel's selected gate from live MMU state."""
if mmu_state is None:
return
self.status = mmu_state
Expand All @@ -134,6 +136,7 @@ def on_mmu_state_changed(self, mmu_state):
self._on_selection(mmu_state.gate)

def addSpool(self, gate_info: GateInfo):
"""Add or refresh a gate's carousel button from its GateInfo."""
self.carousel.addSpool(
QtGui.QColor("#" + str(gate_info.color)[:6]),
gate_info.index,
Expand Down
Loading
Loading