mirror of
https://github.com/Ultimaker/Cura.git
synced 2026-01-06 06:37:46 -07:00
Merge da024debfb into 9edd99f94a
This commit is contained in:
commit
79ab47ec79
30 changed files with 1397 additions and 1180 deletions
241
plugins/MonitorStage/GrblController.py
Normal file
241
plugins/MonitorStage/GrblController.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import time
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
from UM.Logger import Logger
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
|
||||
class GrblController(QObject):
|
||||
connection_status_changed = pyqtSignal(bool)
|
||||
data_received = pyqtSignal(str)
|
||||
|
||||
MAX_BUFFER_SIZE = 20
|
||||
SERIAL_TIMEOUT = 5 # seconds
|
||||
MAX_TEMP_OFFSET = 5 # degrees Celsius
|
||||
JOG_DISTANCE = 40 # mm
|
||||
RECONNECT_INTERVAL = 5 # seconds
|
||||
|
||||
def __init__(self): # No printer_output_device
|
||||
super().__init__()
|
||||
self._serial_port = None # New: Serial port object
|
||||
self.is_connected = False
|
||||
self.last_reconnect_attempt = 0
|
||||
self._received_data_buffer = []
|
||||
self.target_temperatures = {"T0": 20, "T1": 20}
|
||||
self.current_temperatures = {"T0": 0.0, "T1": 0.0}
|
||||
Logger.log("d", "GrblController initialized.")
|
||||
|
||||
def connect(self):
|
||||
Logger.log("d", "Attempting to connect to GRBL device via serial port.")
|
||||
if self._serial_port and self._serial_port.is_open:
|
||||
Logger.log("d", "Already connected to a serial port.")
|
||||
self.is_connected = True
|
||||
self.connection_status_changed.emit(self.is_connected)
|
||||
return
|
||||
|
||||
ports = serial.tools.list_ports.comports()
|
||||
if not ports:
|
||||
Logger.log("w", "No serial ports found.")
|
||||
self.is_connected = False
|
||||
self.connection_status_changed.emit(self.is_connected)
|
||||
return
|
||||
|
||||
for p in ports:
|
||||
Logger.log("d", f"Found port: {p.device}")
|
||||
try:
|
||||
self._serial_port = serial.Serial(p.device, 115200, timeout=1) # 115200 baud rate, 1 second timeout
|
||||
time.sleep(2) # Wait for GRBL to initialize
|
||||
self._serial_port.flushInput() # Clear input buffer
|
||||
Logger.log("d", f"Successfully connected to {p.device}")
|
||||
self.is_connected = True
|
||||
break
|
||||
except serial.SerialException as e:
|
||||
Logger.log("e", f"Failed to connect to {p.device}: {e}")
|
||||
self.is_connected = False
|
||||
|
||||
if self.is_connected:
|
||||
Logger.log("d", "GrblController connected.")
|
||||
# Send GRBL initialization commands and wait for 'ok'
|
||||
self.send_gcode("$22=0") # disable homing
|
||||
self.wait_for_ok()
|
||||
self.send_gcode("$X")
|
||||
self.wait_for_ok()
|
||||
self.send_gcode("$21=0")
|
||||
self.wait_for_ok()
|
||||
self.send_gcode("$3=4") # invert Z axis
|
||||
self.wait_for_ok()
|
||||
self.send_gcode("G21") # mm
|
||||
self.wait_for_ok()
|
||||
self.send_gcode("G90") # absolute coords
|
||||
self.wait_for_ok()
|
||||
else:
|
||||
Logger.log("w", "Could not establish a serial connection to any GRBL device.")
|
||||
|
||||
self.connection_status_changed.emit(self.is_connected)
|
||||
|
||||
def send_gcode(self, command):
|
||||
if self.is_connected and self._serial_port and self._serial_port.is_open:
|
||||
Logger.log("d", f"Sending G-code: {command.strip()}")
|
||||
try:
|
||||
self._serial_port.write(f"{command.strip()}\r\n".encode('utf-8'))
|
||||
except serial.SerialException as e:
|
||||
Logger.log("e", f"Failed to send G-code: {e}")
|
||||
self.is_connected = False
|
||||
self.connection_status_changed.emit(self.is_connected)
|
||||
else:
|
||||
Logger.log("w", f"Cannot send G-code, GrblController not connected: {command.strip()}")
|
||||
|
||||
def get_connection_status(self):
|
||||
return self.is_connected
|
||||
|
||||
def wait_for_ok(self):
|
||||
Logger.log("d", "Waiting for 'ok' from printer...")
|
||||
t_0 = time.time()
|
||||
while time.time() - t_0 < self.SERIAL_TIMEOUT:
|
||||
# Read new data from serial port
|
||||
try:
|
||||
while self._serial_port and self._serial_port.in_waiting:
|
||||
line = self._serial_port.readline().decode('utf-8').strip()
|
||||
if line:
|
||||
Logger.log("d", f"Data received from serial: {line}")
|
||||
self._received_data_buffer.append(line)
|
||||
self.data_received.emit(line)
|
||||
except serial.SerialException as e:
|
||||
Logger.log("e", f"Error reading from serial port: {e}")
|
||||
self.is_connected = False
|
||||
self.connection_status_changed.emit(self.is_connected)
|
||||
return False
|
||||
|
||||
# Check buffer for 'ok'
|
||||
for i, line in enumerate(self._received_data_buffer):
|
||||
if "ok" in line:
|
||||
del self._received_data_buffer[:i+1] # Clear processed data
|
||||
Logger.log("d", "Received 'ok'.")
|
||||
return True
|
||||
time.sleep(0.01) # Small delay to prevent busy-waiting
|
||||
Logger.log("w", "Timeout waiting for 'ok'.")
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
if not self.is_connected:
|
||||
if time.time() - self.last_reconnect_attempt > self.RECONNECT_INTERVAL:
|
||||
self.last_reconnect_attempt = time.time()
|
||||
self.connect()
|
||||
return
|
||||
|
||||
# Read data from serial port
|
||||
try:
|
||||
while self._serial_port and self._serial_port.in_waiting:
|
||||
line = self._serial_port.readline().decode('utf-8').strip()
|
||||
if line:
|
||||
Logger.log("d", f"Data received from serial: {line}")
|
||||
self._received_data_buffer.append(line)
|
||||
self.data_received.emit(line)
|
||||
except serial.SerialException as e:
|
||||
Logger.log("e", f"Error reading from serial port: {e}")
|
||||
self.is_connected = False
|
||||
self.connection_status_changed.emit(self.is_connected)
|
||||
return
|
||||
|
||||
# Process received data for 'ok' and temperature
|
||||
# This part remains largely the same, but now it's processing data from the serial port directly.
|
||||
# Request temperatures
|
||||
self.send_gcode("M105")
|
||||
|
||||
t_0 = time.time()
|
||||
while time.time() - t_0 < self.SERIAL_TIMEOUT:
|
||||
for i, line in enumerate(self._received_data_buffer):
|
||||
if "ok" in line: # Check for 'ok' in the buffer
|
||||
del self._received_data_buffer[:i+1]
|
||||
Logger.log("d", "Received 'ok'.")
|
||||
# If 'ok' is received, we can proceed with other commands or just return
|
||||
# For now, we'll continue to check for M105 response in the same loop
|
||||
pass
|
||||
if "$M105=" in line:
|
||||
# Parse tuple response: $M105=T0:200.000,T1:180.000
|
||||
payload = line.split("=")[-1].strip()
|
||||
pairs = payload.split(",")
|
||||
for pair in pairs:
|
||||
tool, val = pair.split(":")
|
||||
self.current_temperatures[tool] = float(val)
|
||||
del self._received_data_buffer[:i+1]
|
||||
Logger.log("d", f"Received temperature data: {payload}")
|
||||
return
|
||||
time.sleep(0.01)
|
||||
Logger.log("w", "Timeout waiting for M105 response.")
|
||||
|
||||
def set_heating(self, tool: str, temperature: int):
|
||||
"""Set target temperature for a specific tool (e.g., T0, T1)."""
|
||||
self.target_temperatures[tool] = temperature
|
||||
# Send full tuple (all tools), so firmware always has consistent state
|
||||
cmd_parts = [f"{t}:{temp}" for t, temp in self.target_temperatures.items()]
|
||||
cmd = ",".join(cmd_parts)
|
||||
self.send_gcode(f"M104 {cmd}")
|
||||
self.wait_for_ok()
|
||||
|
||||
def handle(self, event):
|
||||
if not self.is_connected:
|
||||
return
|
||||
try:
|
||||
# Placeholder for event classes
|
||||
class UpdateTargetTemperature:
|
||||
def __init__(self, tool, temperature):
|
||||
self.tool = tool
|
||||
self.temperature = temperature
|
||||
class PlayGcode: pass
|
||||
class PauseGcode: pass
|
||||
class Home: pass
|
||||
class NewGcodeFile:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
class Jog:
|
||||
def __init__(self, movement):
|
||||
self.movement = movement
|
||||
|
||||
if isinstance(event, UpdateTargetTemperature):
|
||||
self.set_heating(event.tool, event.temperature)
|
||||
elif isinstance(event, PlayGcode):
|
||||
# self.gcode_handler.play() # GcodeHandler not yet implemented
|
||||
Logger.log("w", "PlayGcode event received, but GcodeHandler is not implemented.")
|
||||
elif isinstance(event, PauseGcode):
|
||||
# self.gcode_handler.pause() # GcodeHandler not yet implemented
|
||||
Logger.log("w", "PauseGcode event received, but GcodeHandler is not implemented.")
|
||||
elif isinstance(event, Home):
|
||||
self.send_gcode("$H")
|
||||
self.wait_for_ok()
|
||||
elif isinstance(event, NewGcodeFile):
|
||||
# self.set_gcode_file(event.filename) # set_gcode_file not yet fully implemented
|
||||
Logger.log("w", f"NewGcodeFile event received for {event.filename}, but set_gcode_file is not fully implemented.")
|
||||
elif isinstance(event, Jog):
|
||||
self.send_gcode("G91") # relative coords
|
||||
self.wait_for_ok()
|
||||
movement = event.movement
|
||||
command = (
|
||||
f"G1 X{movement[0]*self.JOG_DISTANCE} "
|
||||
f"Y{movement[1]*self.JOG_DISTANCE} "
|
||||
f"Z{movement[2]*self.JOG_DISTANCE} "
|
||||
f"E{movement[3]*self.JOG_DISTANCE + 7} "
|
||||
f"B{movement[3]*self.JOG_DISTANCE + 7} "
|
||||
"F200"
|
||||
)
|
||||
self.send_gcode(command)
|
||||
self.wait_for_ok()
|
||||
self.send_gcode("G90") # absolute coords
|
||||
self.wait_for_ok()
|
||||
else:
|
||||
Logger.log("w", "Event not caught: " + str(event))
|
||||
except Exception as e: # Catch all exceptions for now
|
||||
Logger.log("e", f"Error handling event: {e}")
|
||||
self.is_connected = False
|
||||
# self.register_event(events.ArduinoDisconnected()) # events not implemented
|
||||
self.connection_status_changed.emit(self.is_connected)
|
||||
|
||||
def get_aprox_buffer(self):
|
||||
Logger.log("w", "get_aprox_buffer method is a placeholder and not fully implemented for Cura's active printer.")
|
||||
# In a real implementation, this would send a command and parse the response
|
||||
# For now, return a default value
|
||||
return 10 # A reasonable default buffer size
|
||||
|
||||
def set_gcode_file(self, filename: str):
|
||||
Logger.log("w", f"set_gcode_file method is a placeholder and not fully implemented for Cura's active printer. Filename: {filename}")
|
||||
# This would typically involve loading a G-code file and preparing it for printing
|
||||
pass
|
||||
|
|
@ -1,44 +1,17 @@
|
|||
// Copyright (c) 2022 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.0
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.0 as Cura
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
|
||||
Rectangle
|
||||
{
|
||||
id: viewportOverlay
|
||||
|
||||
property bool isConnected: Cura.MachineManager.activeMachineHasNetworkConnection || Cura.MachineManager.activeMachineHasCloudConnection
|
||||
property bool isNetworkConfigurable:
|
||||
{
|
||||
if(Cura.MachineManager.activeMachine === null)
|
||||
{
|
||||
return false
|
||||
}
|
||||
return Cura.MachineManager.activeMachine.supportsNetworkConnection
|
||||
}
|
||||
property var machineManager: Cura.MachineManager
|
||||
property var activeMachine: machineManager.activeMachine
|
||||
property bool isMachineConnected: activeMachine ? activeMachine.is_connected : false
|
||||
|
||||
property bool isNetworkConfigured:
|
||||
{
|
||||
// Readability:
|
||||
var connectedTypes = [2, 3];
|
||||
var types = Cura.MachineManager.activeMachine.configuredConnectionTypes
|
||||
|
||||
// Check if configured connection types includes either 2 or 3 (LAN or cloud)
|
||||
for (var i = 0; i < types.length; i++)
|
||||
{
|
||||
if (connectedTypes.indexOf(types[i]) >= 0)
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
color: UM.Theme.getColor("viewport_overlay")
|
||||
color: "#FAFAFA"
|
||||
anchors.fill: parent
|
||||
|
||||
UM.I18nCatalog
|
||||
|
|
@ -47,124 +20,55 @@ Rectangle
|
|||
name: "cura"
|
||||
}
|
||||
|
||||
// This mouse area is to prevent mouse clicks to be passed onto the scene.
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onWheel: wheel.accepted = true
|
||||
}
|
||||
|
||||
// Disable dropping files into Cura when the monitor page is active
|
||||
DropArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
// CASE 1: CAN MONITOR & CONNECTED
|
||||
Loader
|
||||
{
|
||||
id: monitorViewComponent
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
height: parent.height
|
||||
|
||||
property real maximumWidth: parent.width
|
||||
property real maximumHeight: parent.height
|
||||
|
||||
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem : null
|
||||
}
|
||||
|
||||
// CASE 2 & 3: Empty states
|
||||
Column
|
||||
{
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
topMargin: UM.Theme.getSize("monitor_empty_state_offset").height
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
width: UM.Theme.getSize("monitor_empty_state_size").width
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
visible: monitorViewComponent.sourceComponent == null
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
// CASE 2: CAN MONITOR & NOT CONNECTED
|
||||
UM.Label
|
||||
// Graph Placeholder
|
||||
Rectangle
|
||||
{
|
||||
anchors
|
||||
width: 400
|
||||
height: 200
|
||||
color: "gray"
|
||||
Text
|
||||
{
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
anchors.centerIn: parent
|
||||
text: "Graph Placeholder"
|
||||
color: "white"
|
||||
font.pointSize: 18
|
||||
}
|
||||
visible: isNetworkConfigured && !isConnected
|
||||
text: catalog.i18nc("@info", "Please make sure your printer has a connection:\n- Check if the printer is turned on.\n- Check if the printer is connected to the network.\n- Check if you are signed in to discover cloud-connected printers.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
width: contentWidth
|
||||
}
|
||||
|
||||
UM.Label
|
||||
// Command Buttons
|
||||
Column
|
||||
{
|
||||
id: noNetworkLabel
|
||||
anchors
|
||||
{
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
spacing: 5
|
||||
Text { text: "Command Buttons"; color: "white"; font.pointSize: 16 }
|
||||
Row {
|
||||
spacing: 5
|
||||
Button { text: "Z+" }
|
||||
Button { text: "Z-" }
|
||||
}
|
||||
Row {
|
||||
spacing: 5
|
||||
Button { text: "X+" }
|
||||
Button { text: "X-" }
|
||||
}
|
||||
Row {
|
||||
spacing: 5
|
||||
Button { text: "Y+" }
|
||||
Button { text: "Y-" }
|
||||
}
|
||||
visible: !isNetworkConfigured && isNetworkConfigurable
|
||||
text: catalog.i18nc("@info", "Please connect your printer to the network.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
width: contentWidth
|
||||
}
|
||||
|
||||
Item
|
||||
// Run Buttons
|
||||
Row
|
||||
{
|
||||
anchors
|
||||
{
|
||||
left: noNetworkLabel.left
|
||||
}
|
||||
visible: !isNetworkConfigured && isNetworkConfigurable
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.ColorImage
|
||||
{
|
||||
id: externalLinkIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: UM.Theme.getIcon("LinkExternal")
|
||||
width: UM.Theme.getSize("icon_indicator").width
|
||||
height: UM.Theme.getSize("icon_indicator").height
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
id: manageQueueText
|
||||
anchors
|
||||
{
|
||||
left: externalLinkIcon.right
|
||||
leftMargin: UM.Theme.getSize("narrow_margin").width
|
||||
verticalCenter: externalLinkIcon.verticalCenter
|
||||
}
|
||||
color: UM.Theme.getColor("text_link")
|
||||
font: UM.Theme.getFont("medium")
|
||||
text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
|
||||
}
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: Qt.openUrlExternally("https://ultimaker.com/in/cura/troubleshooting/network?utm_source=cura&utm_medium=software&utm_campaign=monitor-not-connected")
|
||||
onEntered: manageQueueText.font.underline = true
|
||||
onExited: manageQueueText.font.underline = false
|
||||
}
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
id: noConnectionLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !isNetworkConfigurable
|
||||
text: catalog.i18nc("@info", "In order to monitor your print from Cura, please connect the printer.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
wrapMode: Text.WordWrap
|
||||
width: contentWidth
|
||||
spacing: 10
|
||||
Text { text: "Printer Control"; color: "white"; font.pointSize: 16 }
|
||||
Button { text: "Start Printer" }
|
||||
Button { text: "Stop Printer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,27 +7,20 @@ import QtQuick.Controls 2.3
|
|||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
Item
|
||||
Rectangle
|
||||
{
|
||||
signal showTooltip(Item item, point location, string text)
|
||||
signal hideTooltip()
|
||||
id: root
|
||||
|
||||
Cura.MachineSelector
|
||||
{
|
||||
id: machineSelection
|
||||
headerCornerSide: Cura.RoundedRectangle.Direction.All
|
||||
width: UM.Theme.getSize("machine_selector_widget").width
|
||||
height: parent.height
|
||||
property var machineManager: Cura.MachineManager
|
||||
property var activeMachine: machineManager.activeMachine
|
||||
property bool isMachineConnected: activeMachine ? activeMachine.is_connected : false
|
||||
|
||||
color: isMachineConnected ? "green" : "red"
|
||||
|
||||
Label {
|
||||
id: machineStatusLabel
|
||||
text: isMachineConnected ? qsTr("Connected") : qsTr("Disconnected")
|
||||
anchors.centerIn: parent
|
||||
|
||||
machineListModel: Cura.MachineListModel {}
|
||||
|
||||
machineManager: Cura.MachineManager
|
||||
|
||||
onSelectPrinter: function(machine)
|
||||
{
|
||||
toggleContent();
|
||||
Cura.MachineManager.setActiveMachine(machine.id);
|
||||
}
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import os.path
|
||||
from UM.Application import Application
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
from .GrblController import GrblController # New import
|
||||
|
||||
|
||||
class MonitorStage(CuraStage):
|
||||
|
|
@ -13,51 +14,13 @@ class MonitorStage(CuraStage):
|
|||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
self._active_print_job = print_job
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
self._grbl_controller = None # New member variable
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
try:
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
except TypeError:
|
||||
# Ignore stupid "Not connected" errors.
|
||||
pass
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
except IndexError:
|
||||
pass
|
||||
# Instantiate GrblController and connect
|
||||
if self._grbl_controller is None: # Only instantiate once
|
||||
self._grbl_controller = GrblController()
|
||||
self._grbl_controller.connect()
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||
|
|
|
|||
32
plugins/MonitorStage/old-src/application.py
Normal file
32
plugins/MonitorStage/old-src/application.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from core import Controller
|
||||
from ui import Ui # Renamed Ui to PrinterGUI as per previous refactoring
|
||||
import tkinter as tk # Tkinter root will be managed here
|
||||
from logger import Logger
|
||||
|
||||
class PrinterApplication:
|
||||
"""
|
||||
Orchestrates the 3D Printer Controller and its Graphical User Interface.
|
||||
Manages the setup, communication, and lifecycle of the application.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.controller = Controller(Logger("Controller"))
|
||||
self.ui = Ui(Logger("Ui"), update=self.update) # Pass the root to the UI
|
||||
|
||||
self._update_job_id = None
|
||||
self.gcode_pointer = 0 # Manage gcode pointer state within the application
|
||||
|
||||
def update(self):
|
||||
"""TODO"""
|
||||
for event in self.controller.update():
|
||||
self.ui.handle(event)
|
||||
|
||||
for event in self.ui.update():
|
||||
self.controller.handle(event)
|
||||
|
||||
def run(self):
|
||||
"""Initializes and runs the application."""
|
||||
print("Initializing application...")
|
||||
self.controller.connect() # Establish connection to hardware/simulator
|
||||
self.ui.initialize() # Build the UI widgets
|
||||
|
||||
self.ui.run()
|
||||
1
plugins/MonitorStage/old-src/core/__init__.py
Normal file
1
plugins/MonitorStage/old-src/core/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .controller import Controller
|
||||
29
plugins/MonitorStage/old-src/core/commandIterator.py
Normal file
29
plugins/MonitorStage/old-src/core/commandIterator.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from abc import ABC
|
||||
|
||||
"""
|
||||
TODO
|
||||
"""
|
||||
class CommandIterator(ABC):
|
||||
def __init__(self):
|
||||
self.pointer = 0
|
||||
|
||||
def get_pointer(self) -> int:
|
||||
return self.pointer
|
||||
|
||||
def get_text(self, pointer: int) -> str:
|
||||
return ...
|
||||
|
||||
|
||||
"""
|
||||
TODO
|
||||
"""
|
||||
class SweepCommandIterator(ABC):
|
||||
def __init__(self):
|
||||
self.pointer = 0
|
||||
|
||||
def get_pointer(self) -> int:
|
||||
return self.pointer
|
||||
|
||||
def get_text(self, pointer: int) -> str:
|
||||
self.pointer += 1
|
||||
return "G0 X10" if self.get_pointer() % 2 == 0 else "G0 X-10"
|
||||
213
plugins/MonitorStage/old-src/core/controller.py
Normal file
213
plugins/MonitorStage/old-src/core/controller.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import time
|
||||
from .serialBridge import SerialBridge
|
||||
from .filehandler import GcodeHandler
|
||||
import events
|
||||
from logger import NozzleTemperatureWarning
|
||||
from serial.serialutil import SerialException
|
||||
|
||||
class Controller:
|
||||
MAX_BUFFER_SIZE = 20
|
||||
SERIAL_TIMEOUT = 5 # seconds
|
||||
MAX_TEMP_OFFSET = 5 # degrees Celsius
|
||||
JOG_DISTANCE = 40 # mm
|
||||
RECONNECT_INTERVAL = 5 # seconds
|
||||
|
||||
def __init__(self, logger):
|
||||
self.logger = logger
|
||||
self.registered_events = []
|
||||
|
||||
self.set_gcode_file("3d files/hart/hart.gcode")
|
||||
self.serialBridge = SerialBridge()
|
||||
self.is_connected = False
|
||||
self.last_reconnect_attempt = 0
|
||||
|
||||
# Track per-tool target & current temperatures
|
||||
self.target_temperatures = {"T0": 20, "T1": 20}
|
||||
self.current_temperatures = {"T0": 0.0, "T1": 0.0}
|
||||
|
||||
def connect(self):
|
||||
try:
|
||||
self.serialBridge.connect()
|
||||
t_0 = time.time()
|
||||
while "Grbl" not in self.serialBridge.readline():
|
||||
if time.time() - t_0 > Controller.SERIAL_TIMEOUT:
|
||||
raise RuntimeError("Timeout")
|
||||
|
||||
self.serialBridge.write("$22=0\r\n") # disable homing
|
||||
|
||||
self.wait_for_ok()
|
||||
|
||||
self.serialBridge.write("$X\r\n")
|
||||
self.wait_for_ok()
|
||||
|
||||
self.serialBridge.write("$21=0\r\n")
|
||||
self.wait_for_ok()
|
||||
|
||||
self.serialBridge.write("$3=4\r\n") # invert Z axis
|
||||
self.wait_for_ok()
|
||||
|
||||
self.serialBridge.write("G21\r\n") # mm
|
||||
self.wait_for_ok()
|
||||
self.serialBridge.write("G90\r\n") # absolute coords
|
||||
self.wait_for_ok()
|
||||
|
||||
|
||||
self.wait_for_ok()
|
||||
self.is_connected = True
|
||||
self.serialBridge.is_connected = True
|
||||
self.register_event(events.ArduinoConnected())
|
||||
except (RuntimeError, SerialException):
|
||||
self.is_connected = False
|
||||
self.serialBridge.is_connected = False
|
||||
self.register_event(events.ArduinoDisconnected())
|
||||
|
||||
def set_heating(self, tool: str, temperature: int):
|
||||
"""Set target temperature for a specific tool (e.g., T0, T1)."""
|
||||
self.target_temperatures[tool] = temperature
|
||||
# Send full tuple (all tools), so firmware always has consistent state
|
||||
cmd_parts = [f"{t}:{temp}" for t, temp in self.target_temperatures.items()]
|
||||
cmd = ",".join(cmd_parts)
|
||||
self.serialBridge.write(f"M104 {cmd}\r\n")
|
||||
self.wait_for_ok()
|
||||
|
||||
def update(self):
|
||||
if not self.is_connected:
|
||||
if time.time() - self.last_reconnect_attempt > self.RECONNECT_INTERVAL:
|
||||
self.last_reconnect_attempt = time.time()
|
||||
self.connect()
|
||||
return [] # No updates if not connected
|
||||
|
||||
try:
|
||||
self.serialBridge.flush()
|
||||
|
||||
# Update buffer size bookkeeping
|
||||
if self.gcode_handler.aprox_buffer >= Controller.MAX_BUFFER_SIZE:
|
||||
cpy = self.gcode_handler.aprox_buffer
|
||||
self.gcode_handler.aprox_buffer = self.get_aprox_buffer()
|
||||
self.gcode_handler.execution_line += max(0, cpy - self.gcode_handler.aprox_buffer)
|
||||
self.register_event(events.SetGcodeLine(self.gcode_handler.execution_line))
|
||||
|
||||
# Feed commands
|
||||
if self.gcode_handler and self.gcode_handler.playing:
|
||||
if self.gcode_handler.com_line < self.gcode_handler.get_size():
|
||||
t_0 = time.time()
|
||||
while self.gcode_handler.aprox_buffer < Controller.MAX_BUFFER_SIZE:
|
||||
if time.time() - t_0 > Controller.SERIAL_TIMEOUT:
|
||||
raise RuntimeError("Timeout")
|
||||
if self.gcode_handler.com_line >= self.gcode_handler.get_size():
|
||||
break
|
||||
else:
|
||||
command = self.gcode_handler.get_line(self.gcode_handler.com_line)
|
||||
if (
|
||||
len(command) == 0
|
||||
or command[0] == ";"
|
||||
or command[0] == "M"
|
||||
or command[0:3] == "G92"
|
||||
):
|
||||
self.gcode_handler.execution_line += 1
|
||||
else:
|
||||
self.serialBridge.write(command + "\r\n")
|
||||
self.wait_for_ok()
|
||||
self.gcode_handler.aprox_buffer += 1
|
||||
self.gcode_handler.com_line += 1
|
||||
|
||||
# Request temperatures
|
||||
self.serialBridge.write("M105\r\n")
|
||||
t_0 = time.time()
|
||||
while True:
|
||||
if time.time() - t_0 > Controller.SERIAL_TIMEOUT:
|
||||
raise RuntimeError("Timeout")
|
||||
line = self.serialBridge.readline()
|
||||
if "ok\r\n" == line:
|
||||
continue
|
||||
elif "$M105=" in line:
|
||||
# Parse tuple response: $M105=T0:200.000,T1:180.000
|
||||
payload = line.split("=")[-1].strip()
|
||||
pairs = payload.split(",")
|
||||
for pair in pairs:
|
||||
tool, val = pair.split(":")
|
||||
self.current_temperatures[tool] = float(val)
|
||||
break
|
||||
|
||||
# Safety check for each tool
|
||||
for tool, target in self.target_temperatures.items():
|
||||
current = self.current_temperatures.get(tool, 0.0)
|
||||
if abs(target - current) > Controller.MAX_TEMP_OFFSET:
|
||||
self.logger.show_message(NozzleTemperatureWarning(tool))
|
||||
self.register_event(events.UpdateNozzleTemperature(tool, current))
|
||||
|
||||
self.serialBridge.flush()
|
||||
|
||||
except (SerialException, RuntimeError) as e:
|
||||
print(f"Error: {e}")
|
||||
self.is_connected = False
|
||||
self.serialBridge.is_connected = False
|
||||
self.register_event(events.ArduinoDisconnected())
|
||||
|
||||
cpy = self.registered_events
|
||||
self.registered_events = []
|
||||
return cpy
|
||||
|
||||
def register_event(self, event: events.Event):
|
||||
self.registered_events.append(event)
|
||||
|
||||
def handle(self, event: events.Event):
|
||||
if not self.is_connected:
|
||||
return
|
||||
try:
|
||||
match event:
|
||||
case events.UpdateTargetTemperature(tool=tool, temperature=temp):
|
||||
self.set_heating(tool, temp)
|
||||
case events.PlayGcode:
|
||||
self.gcode_handler.play()
|
||||
case events.PauseGcode:
|
||||
self.gcode_handler.pause()
|
||||
case events.Home:
|
||||
self.serialBridge.write("$H\r\n")
|
||||
self.wait_for_ok()
|
||||
case events.NewGcodeFile(filename):
|
||||
self.set_gcode_file(filename)
|
||||
case events.Jog(movement):
|
||||
self.serialBridge.write("G91\r\n") # relative coords
|
||||
self.wait_for_ok()
|
||||
command = (
|
||||
f"G1 X{movement[0]*Controller.JOG_DISTANCE} "
|
||||
f"Y{movement[1]*Controller.JOG_DISTANCE} "
|
||||
f"Z{movement[2]*Controller.JOG_DISTANCE} "
|
||||
f"E{movement[3]*Controller.JOG_DISTANCE + 7} "
|
||||
f"B{movement[3]*Controller.JOG_DISTANCE + 7} "
|
||||
"F200\r\n"
|
||||
)
|
||||
self.serialBridge.write(command)
|
||||
self.wait_for_ok()
|
||||
self.serialBridge.write("G90\r\n") # absolute coords
|
||||
self.wait_for_ok()
|
||||
case _:
|
||||
raise NotImplementedError("Event not caught: " + str(event))
|
||||
except (SerialException, RuntimeError):
|
||||
self.is_connected = False
|
||||
self.serialBridge.is_connected = False
|
||||
self.register_event(events.ArduinoDisconnected())
|
||||
|
||||
def wait_for_ok(self):
|
||||
t_0 = time.time()
|
||||
while "ok\r\n" not in self.serialBridge.readline():
|
||||
if time.time() - t_0 > Controller.SERIAL_TIMEOUT:
|
||||
raise RuntimeError("Timeout")
|
||||
|
||||
def get_aprox_buffer(self):
|
||||
self.serialBridge.flush()
|
||||
self.serialBridge.write("G200\r\n") # custom command
|
||||
t_0 = time.time()
|
||||
while True:
|
||||
if time.time() - t_0 > Controller.SERIAL_TIMEOUT:
|
||||
raise RuntimeError("Timeout")
|
||||
line = self.serialBridge.readline()
|
||||
if "ok\r\n" == line:
|
||||
continue
|
||||
elif "$G200=" in line:
|
||||
return int(line.split("=")[-1][:-2])
|
||||
|
||||
def set_gcode_file(self, filename: str):
|
||||
self.gcode_handler = GcodeHandler(filename)
|
||||
self.register_event(events.NewGcodeFileHandler(self.gcode_handler))
|
||||
47
plugins/MonitorStage/old-src/core/filehandler.py
Normal file
47
plugins/MonitorStage/old-src/core/filehandler.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class GcodeHandler:
|
||||
def __init__(self, filename=None):
|
||||
self.filename = filename
|
||||
|
||||
self.playing = False
|
||||
self.execution_line = -1 # the line that is estimated to be executing
|
||||
self.com_line = 0 # the last line that was sent to the COM
|
||||
self.aprox_buffer = 0 # an estimate of the current planner buffer size in the arduino
|
||||
# this is a theoretical maximum
|
||||
|
||||
with open(filename, 'r') as f:
|
||||
self.gcode_lines = f.read().split('\n')
|
||||
|
||||
def get_line(self, line: int):
|
||||
return self.gcode_lines[line]
|
||||
|
||||
def get_size(self):
|
||||
return len(self.gcode_lines)
|
||||
|
||||
def play(self):
|
||||
if not self.playing:
|
||||
self.playing = True
|
||||
|
||||
def pause(self):
|
||||
if self.playing:
|
||||
self.playing = False
|
||||
|
||||
|
||||
class EmptyGcodeHandler:
|
||||
def __init__(self, filename=None):
|
||||
self.playing = False
|
||||
self.aprox_buffer = 0
|
||||
|
||||
def get_line(self, line: int):
|
||||
return ""
|
||||
|
||||
def get_size(self):
|
||||
return 1
|
||||
|
||||
def play(self):
|
||||
...
|
||||
|
||||
def pause(self):
|
||||
...
|
||||
55
plugins/MonitorStage/old-src/core/serialBridge.py
Normal file
55
plugins/MonitorStage/old-src/core/serialBridge.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import serial
|
||||
from serial.serialutil import SerialException
|
||||
|
||||
SERIAL_PORT = "COM5"
|
||||
|
||||
class SerialBridge:
|
||||
def __init__(self):
|
||||
self.ser = None
|
||||
|
||||
def connect(self):
|
||||
self.ser = serial.Serial(
|
||||
SERIAL_PORT,
|
||||
baudrate=115200,
|
||||
timeout=1
|
||||
)
|
||||
self._buffer = ""
|
||||
|
||||
def disconnect(self):
|
||||
if self.ser and self.ser.is_open:
|
||||
self.ser.close()
|
||||
|
||||
def read(self):
|
||||
if not self.ser:
|
||||
raise SerialException("Not connected")
|
||||
return self.ser.read()
|
||||
|
||||
def write(self, data):
|
||||
if not self.ser:
|
||||
raise SerialException("Not connected")
|
||||
print(f"> {data.strip()}")
|
||||
return self.ser.write(data.encode("utf-8"))
|
||||
|
||||
def readline(self) -> str:
|
||||
char = True
|
||||
while char:
|
||||
char = self.read()
|
||||
if char:
|
||||
# print(char)
|
||||
try:
|
||||
char = char.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue # Ignore non-utf8 characters
|
||||
self._buffer += char
|
||||
|
||||
if char == '\n':
|
||||
cpy = self._buffer
|
||||
print(f"< {cpy.strip()}")
|
||||
self._buffer = "" # clear buffer
|
||||
return cpy
|
||||
return ""
|
||||
|
||||
def flush(self):
|
||||
if self.ser:
|
||||
self.ser.flush()
|
||||
|
||||
57
plugins/MonitorStage/old-src/events.py
Normal file
57
plugins/MonitorStage/old-src/events.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
# Base Event class (optional, but good for type hinting)
|
||||
@dataclass(frozen=True) # frozen=True makes them immutable and hashable
|
||||
class Event:
|
||||
pass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UpdateNozzleTemperature(Event):
|
||||
tool: str # e.g. "T0", "T1"
|
||||
temperature: float # current temperature of that tool
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UpdateTargetTemperature(Event):
|
||||
tool: str # e.g. "T0", "T1"
|
||||
temperature: float # target temperature for that tool
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewGcodeFileHandler(Event):
|
||||
handler: object
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SetGcodeLine(Event):
|
||||
line: int
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewGcodeFile(Event):
|
||||
filename: str
|
||||
|
||||
#
|
||||
@dataclass(frozen=True)
|
||||
class Jog(Event):
|
||||
movement: list # [delta_x, delta_y, delta_z, delta_z]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Home(Event):
|
||||
...
|
||||
|
||||
|
||||
# GCODE ACTIONS
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlayGcode(Event):
|
||||
...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PauseGcode(Event):
|
||||
...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArduinoConnected(Event):
|
||||
...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArduinoDisconnected(Event):
|
||||
...
|
||||
BIN
plugins/MonitorStage/old-src/images/logo.ico
Normal file
BIN
plugins/MonitorStage/old-src/images/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
plugins/MonitorStage/old-src/images/logo.png
Normal file
BIN
plugins/MonitorStage/old-src/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1,009 KiB |
48
plugins/MonitorStage/old-src/logger.py
Normal file
48
plugins/MonitorStage/old-src/logger.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
class Logger:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.shown_messages = set()
|
||||
|
||||
def show_message(self, message: object):
|
||||
if issubclass(type(message), LogMessage):
|
||||
print("ℹ️ " + message.content)
|
||||
elif issubclass(type(message), WarningMessage):
|
||||
for msg in self.shown_messages:
|
||||
if type(message) == type(msg):
|
||||
# already shown
|
||||
return
|
||||
# show `message`
|
||||
print("⚠️ "+message.content)
|
||||
self.shown_messages.add(message)
|
||||
else:
|
||||
raise NotImplementedError("Event not catched: ")
|
||||
|
||||
|
||||
## MESSAGES ##
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Message:
|
||||
content: str
|
||||
|
||||
## LOG MESSAGES ##
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LogMessage(Message):
|
||||
...
|
||||
|
||||
## WARNING MESSAGES ##
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WarningMessage(Message):
|
||||
pass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NozzleTemperatureWarning(WarningMessage):
|
||||
def __init__(self, tool):
|
||||
super().__init__(
|
||||
f"The temperature for {tool} is too far from the target temperature."
|
||||
)
|
||||
|
||||
## ERROR MESSAGES ##
|
||||
8
plugins/MonitorStage/old-src/main.py
Normal file
8
plugins/MonitorStage/old-src/main.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from application import PrinterApplication
|
||||
|
||||
def main():
|
||||
app = PrinterApplication()
|
||||
app.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
plugins/MonitorStage/old-src/ui/__init__.py
Normal file
1
plugins/MonitorStage/old-src/ui/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .tkinter_ui import TkinterUi as Ui
|
||||
10
plugins/MonitorStage/old-src/ui/abstract_ui.py
Normal file
10
plugins/MonitorStage/old-src/ui/abstract_ui.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
# Define the abstract base class for the GUI interface
|
||||
class AbstractUI(ABC):
|
||||
@abstractmethod
|
||||
def update(self):
|
||||
"""
|
||||
Abstract method: Updates the GUI elements.
|
||||
"""
|
||||
pass
|
||||
123
plugins/MonitorStage/old-src/ui/actions_control.py
Normal file
123
plugins/MonitorStage/old-src/ui/actions_control.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
class ActionsControl:
|
||||
"""Manages the Play, Pause, Stop, and Jog buttons."""
|
||||
def __init__(
|
||||
self,
|
||||
parent_frame: ttk.Frame,
|
||||
play_callback: callable = None,
|
||||
home_callback: callable = None,
|
||||
pause_callback: callable = None,
|
||||
stop_callback: callable = None,
|
||||
jog_callback: callable = None,
|
||||
open_file_callback: callable = None,
|
||||
):
|
||||
self.parent_frame = parent_frame
|
||||
|
||||
self.play_callback = play_callback
|
||||
self.home_callback = home_callback
|
||||
self.pause_callback = pause_callback
|
||||
self.stop_callback = stop_callback
|
||||
self.jog_callback = jog_callback # Accept a general jog callback
|
||||
self.open_file_callback = open_file_callback
|
||||
|
||||
self.playing = False
|
||||
|
||||
ttk.Label(parent_frame, text="Actions", style='Heading.TLabel').pack(anchor=tk.NW, pady=(0, 10))
|
||||
|
||||
button_frame = ttk.Frame(parent_frame, style='DarkFrame.TFrame')
|
||||
button_frame.pack(fill=tk.X, pady=(5, 0))
|
||||
|
||||
self.play_button = ttk.Button(
|
||||
button_frame,
|
||||
text="▶ Play",
|
||||
command=self._on_play,
|
||||
style='DarkButton.TButton'
|
||||
)
|
||||
self.play_button.pack(side=tk.LEFT, expand=True, padx=5, pady=5)
|
||||
|
||||
self.open_file_button = ttk.Button(
|
||||
button_frame,
|
||||
text="📄 Open",
|
||||
command=self._on_open_file,
|
||||
style='DarkButton.TButton'
|
||||
)
|
||||
self.open_file_button.pack(side=tk.LEFT, expand=True, padx=5, pady=5)
|
||||
|
||||
# Jog buttons section
|
||||
jog_frame = ttk.Frame(parent_frame, style='DarkFrame.TFrame')
|
||||
jog_frame.pack(pady=(10, 0))
|
||||
|
||||
directions = [
|
||||
("X-", [-1, 0, 0, 0], 0, 0), ("X+", [1, 0, 0, 0], 0, 1),
|
||||
("Y-", [0, -1, 0, 0], 1, 0), ("Y+", [0, 1, 0, 0], 1, 1),
|
||||
("Z-", [0, 0, -1, 0], 2, 0), ("Z+", [0, 0, 1, 0], 2, 1),
|
||||
("E-", [0, 0, 0, -1], 3, 0), ("E+", [0, 0, 0, 1], 3, 1),
|
||||
]
|
||||
|
||||
for label, movement, row, col in directions:
|
||||
btn = ttk.Button(
|
||||
jog_frame,
|
||||
text=label,
|
||||
command=lambda movement=movement: self._on_jog(movement),
|
||||
style='DarkButton.TButton'
|
||||
)
|
||||
btn.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
|
||||
|
||||
# Optional: make buttons stretch to fill the frame
|
||||
for i in range(3):
|
||||
jog_frame.rowconfigure(i, weight=1)
|
||||
for i in range(2):
|
||||
jog_frame.columnconfigure(i, weight=1)
|
||||
|
||||
|
||||
self.home_button = ttk.Button(
|
||||
button_frame,
|
||||
text="🏠 Home",
|
||||
command=self._on_home,
|
||||
style='DarkButton.TButton'
|
||||
)
|
||||
self.home_button.pack(side=tk.LEFT, expand=True, padx=5, pady=5)
|
||||
|
||||
def _on_play(self):
|
||||
if self.playing:
|
||||
# pause
|
||||
if self.pause_callback:
|
||||
self.pause_callback()
|
||||
self.play_button.configure(text="▶ Play")
|
||||
self.playing = False
|
||||
else:
|
||||
# play
|
||||
if self.play_callback:
|
||||
self.play_callback()
|
||||
self.play_button.configure(text="⏸ Pause")
|
||||
self.playing = True
|
||||
|
||||
|
||||
def _on_home(self):
|
||||
if self.home_callback:
|
||||
self.home_callback()
|
||||
|
||||
def _on_jog(self, axis):
|
||||
if self.jog_callback:
|
||||
self.jog_callback(axis)
|
||||
|
||||
def _on_open_file(self):
|
||||
file = tk.filedialog.askopenfilename(
|
||||
parent=self.parent_frame,
|
||||
# mode='r',
|
||||
title="Select a file",
|
||||
initialdir="C:\\Users\\Simon\\Documents\\projecten\\Chocolate-Printer\\3d files\\",
|
||||
filetypes=(("Gcode Slice", "*.gcode"),),
|
||||
defaultextension="gcode",
|
||||
multiple=False
|
||||
)
|
||||
if file is None:
|
||||
# user canceled selection
|
||||
return
|
||||
|
||||
if self.open_file_callback is None:
|
||||
raise ValueError()
|
||||
|
||||
self.open_file_callback(file)
|
||||
56
plugins/MonitorStage/old-src/ui/dark_theme.py
Normal file
56
plugins/MonitorStage/old-src/ui/dark_theme.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from tkinter import ttk
|
||||
|
||||
|
||||
class DarkTheme:
|
||||
"""Manages the application's dark theme styles for Tkinter widgets."""
|
||||
def __init__(self, root):
|
||||
self.style = ttk.Style()
|
||||
self._configure_styles()
|
||||
root.tk_setPalette(background='#2c313a', foreground='#ffffff')
|
||||
|
||||
def _configure_styles(self):
|
||||
"""Applies the dark theme configurations to ttk widgets."""
|
||||
FONT_SIZE = 16
|
||||
self.style.theme_use('clam')
|
||||
self.style.configure('DarkFrame.TFrame', background='#2c313a', relief='flat')
|
||||
self.style.configure('DarkLabel.TLabel', background='#2c313a', foreground='#ffffff', font=('Inter', 16))
|
||||
self.style.configure('Heading.TLabel', background='#2c313a', foreground='#ffffff', font=('Inter', FONT_SIZE, 'bold'))
|
||||
self.style.configure('DarkText.TText', background='#1e2127', foreground='#ffffff', insertbackground='#ffffff', font=('monospace', 10))
|
||||
self.style.configure('DarkButton.TButton', background='#4a90e2', foreground='#ffffff',
|
||||
font=('Inter', FONT_SIZE, 'bold'), borderwidth=0, focusthickness=3, focuscolor='none')
|
||||
self.style.map('DarkButton.TButton',
|
||||
background=[('active', '#357abd')],
|
||||
foreground=[('active', '#ffffff')])
|
||||
|
||||
# ----- Scale -----
|
||||
self.style.layout('DarkScale.Horizontal.TScale',
|
||||
[('Horizontal.Scale.trough',
|
||||
{'children': [('Horizontal.Scale.slider',
|
||||
{'side': 'left', 'sticky': 'ns'})],
|
||||
'sticky': 'nswe'})])
|
||||
|
||||
self.style.configure('DarkScale.Horizontal.TScale',
|
||||
background='#2c313a', troughcolor='#1e2127',
|
||||
slidercolor='#4a90e2',
|
||||
foreground='#ffffff', highlightbackground='#2c313a',
|
||||
borderwidth=0)
|
||||
self.style.map('DarkScale.Horizontal.TScale',
|
||||
slidercolor=[('active', '#357abd')])
|
||||
|
||||
# ----- Progressbar -----
|
||||
self.style.layout('Dark.Horizontal.TProgressbar',
|
||||
[('Horizontal.Progressbar.trough',
|
||||
{'children': [('Horizontal.Progressbar.pbar',
|
||||
{'side': 'left', 'sticky': 'ns'})],
|
||||
'sticky': 'nswe'})])
|
||||
|
||||
self.style.configure('Dark.Horizontal.TProgressbar',
|
||||
troughcolor='#1e2127',
|
||||
background='#4a90e2',
|
||||
bordercolor='#2c313a',
|
||||
lightcolor='#4a90e2',
|
||||
darkcolor='#4a90e2',
|
||||
thickness=20)
|
||||
|
||||
self.style.map('Dark.Horizontal.TProgressbar',
|
||||
background=[('active', '#357abd')])
|
||||
25
plugins/MonitorStage/old-src/ui/gcode/__init__.py
Normal file
25
plugins/MonitorStage/old-src/ui/gcode/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from .gcode_viewer import GcodeViewer
|
||||
|
||||
|
||||
|
||||
class GcodeFrame:
|
||||
"""Manages the G-code text area, highlighting, and scrolling."""
|
||||
def __init__(self, parent_frame: ttk.Frame):
|
||||
|
||||
title = ttk.Label(parent_frame, text="G-code Execution", style='Heading.TLabel')
|
||||
title.pack(anchor=tk.NW, pady=(0, 10))
|
||||
|
||||
self.filename_label = tk.Label(parent_frame, text="No file selected")
|
||||
self.filename_label.pack(anchor=tk.NW, pady=(0, 10))
|
||||
|
||||
self.gcode_viewer = GcodeViewer(parent_frame)
|
||||
|
||||
|
||||
def set_fileHandler(self, filehandler: object):
|
||||
self.filename_label.configure(text=filehandler.filename)
|
||||
self.gcode_viewer.set_fileHandler(filehandler)
|
||||
|
||||
def set_gcode_pointer(self, pointer: int):
|
||||
self.gcode_viewer.set_gcode_pointer(pointer)
|
||||
71
plugins/MonitorStage/old-src/ui/gcode/gcode_viewer.py
Normal file
71
plugins/MonitorStage/old-src/ui/gcode/gcode_viewer.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
class GcodeViewer:
|
||||
"""Manages the G-code text area, highlighting, and scrolling."""
|
||||
def __init__(self, parent_frame: ttk.Frame):
|
||||
self.filehandler = None
|
||||
self.current_gcode_pointer = -1
|
||||
|
||||
self.gcode_text = tk.Text(parent_frame, wrap=tk.WORD, height=7, width=80,
|
||||
background='#1e2127', foreground='#ffffff',
|
||||
insertbackground='#ffffff', font=('monospace', 10),
|
||||
borderwidth=0, highlightthickness=0, relief='flat')
|
||||
self.gcode_text.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||
|
||||
self.gcode_text.tag_configure("current_line", background="#3a4049", foreground="#2ecc71", font=('monospace', 10, 'bold'))
|
||||
self.gcode_text.tag_configure("comment", foreground="#888888")
|
||||
|
||||
self.progress_bar = ttk.Progressbar(
|
||||
parent_frame,
|
||||
orient="horizontal",
|
||||
length=200,
|
||||
mode="determinate",
|
||||
style='Dark.Horizontal.TProgressbar',
|
||||
takefocus=True,
|
||||
maximum=100,
|
||||
# height=10,
|
||||
)
|
||||
self.progress_bar.pack(fill=tk.X, expand=True, pady=(0, 10))
|
||||
|
||||
def set_gcode_pointer(self, pointer: int):
|
||||
"""
|
||||
Sets the pointer in the gcode to a specific line position.
|
||||
Auto-scrolls to the highlighted line.
|
||||
"""
|
||||
self.gcode_text.tag_remove("current_line", "1.0", tk.END)
|
||||
|
||||
if 0 <= pointer < self.filehandler.get_size():
|
||||
self.current_gcode_pointer = pointer
|
||||
start_index = f"{pointer + 1}.0"
|
||||
end_index = f"{pointer + 1}.end"
|
||||
self.gcode_text.tag_add("current_line", start_index, end_index)
|
||||
self.gcode_text.see(start_index)
|
||||
|
||||
# set progress bar
|
||||
percentage = pointer/self.filehandler.get_size()*100
|
||||
self.progress_bar["value"] = percentage
|
||||
else:
|
||||
self.current_gcode_pointer = -1
|
||||
|
||||
def set_fileHandler(self, filehandler: object):
|
||||
"""TODO"""
|
||||
self.filehandler = filehandler
|
||||
|
||||
self.gcode_text.config(state=tk.NORMAL) # enable editing
|
||||
|
||||
self.gcode_text.delete('1.0', tk.END) # clear all contents
|
||||
|
||||
# Populates the G-code text area with lines from the G-code file.
|
||||
for i in range(self.filehandler.get_size()):
|
||||
self.gcode_text.insert(tk.END, self.filehandler.get_line(i) + "\n")
|
||||
|
||||
# Applies comment highlighting to G-code lines.
|
||||
for i in range(self.filehandler.get_size()):
|
||||
line = self.filehandler.get_line(i)
|
||||
if ';' in line:
|
||||
comment_start = line.find(';')
|
||||
self.gcode_text.tag_add("comment", f"{i+1}.{comment_start}", f"{i+1}.end")
|
||||
|
||||
self.gcode_text.config(state=tk.DISABLED)
|
||||
self.set_gcode_pointer(0)
|
||||
63
plugins/MonitorStage/old-src/ui/heating_control.py
Normal file
63
plugins/MonitorStage/old-src/ui/heating_control.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
class HeatingControl:
|
||||
"""Manages heating sliders for multiple tools independently."""
|
||||
def __init__(self, parent_frame: ttk.Frame, on_update=None):
|
||||
self._on_update = on_update
|
||||
|
||||
# Initial slider values
|
||||
self.slider_values = {"T0": 20, "T1": 20}
|
||||
|
||||
# --- T0 Slider ---
|
||||
ttk.Label(parent_frame, text="T0 Temperature", style='DarkLabel.TLabel').pack(anchor=tk.NW, pady=(5, 0))
|
||||
self.slider_t0 = tk.Scale(
|
||||
parent_frame,
|
||||
from_=20,
|
||||
to=50,
|
||||
orient=tk.HORIZONTAL,
|
||||
command=lambda v: self._on_slider_change("T0", v),
|
||||
tickinterval=5,
|
||||
showvalue=0,
|
||||
relief=tk.RIDGE,
|
||||
bd=2
|
||||
)
|
||||
self.slider_t0.set(self.slider_values["T0"])
|
||||
self.slider_t0.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
self.label_t0 = ttk.Label(parent_frame, text=f"T0 Heating Level: {self.slider_values['T0']}°C", style='DarkLabel.TLabel')
|
||||
self.label_t0.pack(anchor=tk.NW, pady=(0, 10))
|
||||
|
||||
# --- T1 Slider ---
|
||||
ttk.Label(parent_frame, text="T1 Temperature", style='DarkLabel.TLabel').pack(anchor=tk.NW, pady=(5, 0))
|
||||
self.slider_t1 = tk.Scale(
|
||||
parent_frame,
|
||||
from_=20,
|
||||
to=50,
|
||||
orient=tk.HORIZONTAL,
|
||||
command=lambda v: self._on_slider_change("T1", v),
|
||||
tickinterval=5,
|
||||
showvalue=0,
|
||||
relief=tk.RIDGE,
|
||||
bd=2
|
||||
)
|
||||
self.slider_t1.set(self.slider_values["T1"])
|
||||
self.slider_t1.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
self.label_t1 = ttk.Label(parent_frame, text=f"T1 Heating Level: {self.slider_values['T1']}°C", style='DarkLabel.TLabel')
|
||||
self.label_t1.pack(anchor=tk.NW, pady=(0, 10))
|
||||
|
||||
def _on_slider_change(self, tool, value):
|
||||
"""Callback when a slider value changes."""
|
||||
level = int(float(value))
|
||||
self.slider_values[tool] = level
|
||||
|
||||
# Update label
|
||||
if tool == "T0":
|
||||
self.label_t0.config(text=f"T0 Heating Level: {level}°C")
|
||||
elif tool == "T1":
|
||||
self.label_t1.config(text=f"T1 Heating Level: {level}°C")
|
||||
|
||||
# Call update callback if provided
|
||||
if self._on_update:
|
||||
self._on_update(tool=tool, level=level)
|
||||
112
plugins/MonitorStage/old-src/ui/temperature_chart.py
Normal file
112
plugins/MonitorStage/old-src/ui/temperature_chart.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
from datetime import datetime
|
||||
from tkinter import ttk
|
||||
from collections import deque
|
||||
|
||||
class TemperatureChart:
|
||||
"""Manages the Matplotlib temperature chart for multiple tools."""
|
||||
COLORS = {"T0": "#e74c3c", "T1": "#3498db"} # T0=red, T1=blue
|
||||
|
||||
def __init__(self, parent_frame: ttk.Frame, max_chart_points: int = 60):
|
||||
self.max_chart_points = max_chart_points
|
||||
self.temperature_data = {tool: deque(maxlen=max_chart_points) for tool in TemperatureChart.COLORS}
|
||||
self.time_data = deque(maxlen=max_chart_points)
|
||||
self.target_temperatures = {tool: None for tool in TemperatureChart.COLORS}
|
||||
self.lines = {}
|
||||
self.target_lines = {}
|
||||
|
||||
# Figure setup
|
||||
self.fig, self.ax = plt.subplots(figsize=(5, 3), dpi=100)
|
||||
self._configure_chart_style()
|
||||
|
||||
# Create one line per tool
|
||||
for tool, color in TemperatureChart.COLORS.items():
|
||||
line, = self.ax.plot([], [], color=color, linewidth=2, label=f"{tool} Current")
|
||||
self.lines[tool] = line
|
||||
|
||||
self.canvas = FigureCanvasTkAgg(self.fig, master=parent_frame)
|
||||
self.canvas_widget = self.canvas.get_tk_widget()
|
||||
self.canvas_widget.pack(fill="both", expand=True)
|
||||
|
||||
# Labels per tool
|
||||
self.temp_labels = {}
|
||||
for tool in TemperatureChart.COLORS:
|
||||
label = ttk.Label(parent_frame, text=f"{tool}: --.--°C", style='DarkLabel.TLabel')
|
||||
label.pack(anchor="s", pady=(5, 0))
|
||||
self.temp_labels[tool] = label
|
||||
|
||||
self.ax.legend(loc='upper left', facecolor='#2c313a', edgecolor='#cccccc', labelcolor='white', framealpha=0.8)
|
||||
|
||||
def _configure_chart_style(self):
|
||||
self.ax.set_facecolor('#1e2127')
|
||||
self.fig.patch.set_facecolor('#1e2127')
|
||||
self.ax.tick_params(axis='x', colors='#cccccc')
|
||||
self.ax.tick_params(axis='y', colors='#cccccc')
|
||||
self.ax.spines['bottom'].set_color('#cccccc')
|
||||
self.ax.spines['left'].set_color('#cccccc')
|
||||
self.ax.spines['top'].set_visible(False)
|
||||
self.ax.spines['right'].set_visible(False)
|
||||
|
||||
self.ax.set_xlabel("Time", color='#cccccc')
|
||||
self.ax.set_ylabel("Temperature (°C)", color='#cccccc')
|
||||
self.ax.set_title("Temperature Readings", color='#ffffff')
|
||||
self.ax.set_ylim(0, 50)
|
||||
|
||||
def add_temperatures(self, temps: dict):
|
||||
"""
|
||||
Add temperatures for all tools at the current time step.
|
||||
temps: dict mapping tool -> temperature
|
||||
"""
|
||||
now = datetime.now()
|
||||
self.time_data.append(now)
|
||||
|
||||
for tool in self.temperature_data:
|
||||
if tool in temps:
|
||||
self.temperature_data[tool].append(temps[tool])
|
||||
else:
|
||||
# Repeat last value if no new reading
|
||||
last = self.temperature_data[tool][-1] if self.temperature_data[tool] else 0.0
|
||||
self.temperature_data[tool].append(last)
|
||||
|
||||
# Update label if tool was provided
|
||||
if tool in temps:
|
||||
self.temp_labels[tool].config(text=f"{tool}: {temps[tool]:.1f}°C")
|
||||
|
||||
# Update all lines
|
||||
for tool, line in self.lines.items():
|
||||
line.set_data(list(self.time_data), list(self.temperature_data[tool]))
|
||||
|
||||
# Update x-axis
|
||||
if self.time_data:
|
||||
self.ax.set_xlim(self.time_data[0], self.time_data[-1])
|
||||
num_ticks = 5
|
||||
tick_indices = [int(i*(len(self.time_data)-1)/(num_ticks-1)) for i in range(num_ticks)] if len(self.time_data) > 1 else [0]
|
||||
self.ax.set_xticks([self.time_data[i] for i in tick_indices])
|
||||
self.ax.set_xticklabels([self.time_data[i].strftime("%H:%M:%S") for i in tick_indices], rotation=45, ha='right')
|
||||
self.ax.figure.autofmt_xdate()
|
||||
|
||||
self.canvas.draw_idle()
|
||||
|
||||
|
||||
|
||||
def set_target_temperature(self, tool: str, target_temp: float):
|
||||
"""Set target temperature for a specific tool."""
|
||||
self.target_temperatures[tool] = target_temp
|
||||
|
||||
if tool in self.target_lines and self.target_lines[tool]:
|
||||
self.target_lines[tool].remove()
|
||||
|
||||
self.target_lines[tool] = self.ax.axhline(
|
||||
y=target_temp,
|
||||
color=TemperatureChart.COLORS[tool],
|
||||
linestyle='--',
|
||||
linewidth=1.5,
|
||||
label=f"{tool} Target: {target_temp:.1f}°C"
|
||||
)
|
||||
|
||||
self.ax.legend(loc='upper left', facecolor='#2c313a', edgecolor='#cccccc', labelcolor='white', framealpha=0.8)
|
||||
self.canvas.draw_idle()
|
||||
|
||||
def close(self):
|
||||
plt.close(self.fig)
|
||||
139
plugins/MonitorStage/old-src/ui/tkinter_ui.py
Normal file
139
plugins/MonitorStage/old-src/ui/tkinter_ui.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
from .abstract_ui import AbstractUI
|
||||
from .dark_theme import DarkTheme
|
||||
from .heating_control import HeatingControl
|
||||
from .temperature_chart import TemperatureChart
|
||||
from .gcode import GcodeFrame
|
||||
import events
|
||||
from .actions_control import ActionsControl
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
class TkinterUi(AbstractUI):
|
||||
MIN_WIDTH = 1000
|
||||
MIN_HEIGHT = 800
|
||||
|
||||
def __init__(self, logger, update:callable):
|
||||
self.logger = logger
|
||||
self.registered_events = []
|
||||
|
||||
self.root = tk.Tk()
|
||||
self.root.title("3D Printer Control Panel")
|
||||
self.root.geometry(f"{TkinterUi.MIN_WIDTH}x{TkinterUi.MIN_HEIGHT}")
|
||||
self.root.minsize(TkinterUi.MIN_WIDTH, TkinterUi.MIN_HEIGHT)
|
||||
self.root.configure(bg="#282c36")
|
||||
|
||||
self.status_label = tk.Label(self.root, text="Disconnected", fg="red", bg="#282c36", font=("Arial", 10))
|
||||
|
||||
self._update_job_id = None
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
||||
self._running = True
|
||||
self._on_update = update
|
||||
|
||||
def initialize(self):
|
||||
"""Creates and lays out all the GUI elements using specialized components."""
|
||||
|
||||
# Apply dark theme using the global ttk.Style instance
|
||||
self.theme = DarkTheme(self.root) # Call without arguments now
|
||||
|
||||
# Main Layout Frames
|
||||
top_frame = ttk.Frame(self.root, padding="10 10 10 10", style='DarkFrame.TFrame')
|
||||
top_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
bottom_frame = ttk.Frame(self.root, padding="10 10 10 10", style='DarkFrame.TFrame')
|
||||
bottom_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Temperature Chart Section
|
||||
temp_chart_frame = ttk.Frame(top_frame, padding="15", style='DarkFrame.TFrame')
|
||||
temp_chart_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
ttk.Label(temp_chart_frame, text="🔴 Current Temperature (Live Chart)", style='Heading.TLabel', foreground='#e74c3c').pack(anchor=tk.NW, pady=(0, 10))
|
||||
self.temperature_chart = TemperatureChart(temp_chart_frame)
|
||||
|
||||
actions_frame = ttk.Frame(top_frame, padding="0 15 0 0", style='DarkFrame.TFrame') # Add some top padding
|
||||
actions_frame.pack(fill=tk.X, pady=(10,0)) # Fill horizontally within settings_frame
|
||||
# Initialize ActionsControl, passing initial callbacks (which might be None at this point)
|
||||
self.actions_control = ActionsControl(
|
||||
actions_frame,
|
||||
play_callback=lambda: self.register_event(events.PlayGcode),
|
||||
home_callback=lambda: self.register_event(events.Home),
|
||||
pause_callback=lambda: self.register_event(events.PauseGcode),
|
||||
jog_callback=lambda movement: self.register_event(events.Jog(movement)),
|
||||
open_file_callback=lambda filename: self.register_event(events.NewGcodeFile(filename)),
|
||||
)
|
||||
|
||||
# Printer Settings Section
|
||||
settings_frame = ttk.Frame(top_frame, padding="15", style='DarkFrame.TFrame')
|
||||
settings_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
# ttk.Label(settings_frame, text="Printer Settings", style='Heading.TLabel').pack(anchor=tk.NW, pady=(0, 10))
|
||||
|
||||
# Pass the slider_callback to HeatingControl
|
||||
def update_heating_temp(tool, level):
|
||||
# update horizontal line in graph
|
||||
self.temperature_chart.set_target_temperature(tool, level)
|
||||
# register event for controller
|
||||
self.register_event(events.UpdateTargetTemperature(tool, level))
|
||||
self.heating_control = HeatingControl(settings_frame, on_update=update_heating_temp)
|
||||
|
||||
# G-code Execution Section
|
||||
self.gcode_frame = ttk.Frame(bottom_frame, padding="15", style='DarkFrame.TFrame')
|
||||
self.gcode_viewer = GcodeFrame(self.gcode_frame)
|
||||
self.gcode_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
self.status_label.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
|
||||
def run(self):
|
||||
"""TODO"""
|
||||
self._periodic_update()
|
||||
self.root.mainloop()
|
||||
|
||||
def handle(self, event: events.Event):
|
||||
match event:
|
||||
case events.UpdateNozzleTemperature(tool=tool, temperature=temp):
|
||||
self.temperature_chart.add_temperatures({tool: temp})
|
||||
case events.NewGcodeFileHandler(handler=handler):
|
||||
self.gcode_viewer.set_fileHandler(handler)
|
||||
case events.SetGcodeLine(line=line):
|
||||
self.gcode_viewer.set_gcode_pointer(line)
|
||||
case events.ArduinoConnected():
|
||||
self.status_label.config(text="Connected", fg="green")
|
||||
case events.ArduinoDisconnected():
|
||||
self.status_label.config(text="Disconnected", fg="red")
|
||||
case _:
|
||||
raise NotImplementedError("Event not caught: " + str(event))
|
||||
|
||||
|
||||
def register_event(self, event: events.Event):
|
||||
self.registered_events.append(event)
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Updates the GUI. This is typically handled by the Tkinter event loop,
|
||||
but can be called explicitly for immediate redraws if necessary.
|
||||
"""
|
||||
self.root.update_idletasks()
|
||||
|
||||
# return all registered events and clear
|
||||
cpy = self.registered_events
|
||||
self.registered_events = []
|
||||
return cpy
|
||||
|
||||
def _periodic_update(self):
|
||||
"""
|
||||
Performs periodic updates for the controller and UI.
|
||||
This simulates continuous data flow and G-code progression.
|
||||
"""
|
||||
if self._running:
|
||||
self._on_update()
|
||||
|
||||
# Schedule the next update
|
||||
if self._running:
|
||||
self._update_job_id = self.root.after(500, self._periodic_update)
|
||||
else:
|
||||
print("UI is not running, stopping periodic updates.")
|
||||
|
||||
def _on_closing(self):
|
||||
self._running = False
|
||||
self.temperature_chart.close() # Close Matplotlib figure
|
||||
self.root.destroy()
|
||||
if self._update_job_id:
|
||||
self.root.after_cancel(self._update_job_id)
|
||||
|
|
@ -9,6 +9,5 @@ def getMetaData():
|
|||
|
||||
|
||||
def register(app):
|
||||
# We are violating the QT API here (as we use a factory, which is technically not allowed).
|
||||
# but we don't really have another means for doing this (and it seems to you know -work-)
|
||||
return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager(app)}
|
||||
# USBPrinting plugin disabled: do not register any output devices
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,26 +1 @@
|
|||
{
|
||||
"metadata": {
|
||||
"name": "Colorblind Assist Dark",
|
||||
"inherits": "cura-dark"
|
||||
},
|
||||
|
||||
"colors": {
|
||||
"x_axis": [212, 0, 0, 255],
|
||||
"y_axis": [64, 64, 255, 255],
|
||||
|
||||
"model_overhang": [200, 0, 255, 255],
|
||||
|
||||
"xray": [26, 26, 62, 255],
|
||||
"xray_error": [255, 0, 0, 255],
|
||||
|
||||
"layerview_inset_0": [255, 64, 0, 255],
|
||||
"layerview_inset_x": [0, 156, 128, 255],
|
||||
"layerview_skin": [255, 255, 86, 255],
|
||||
"layerview_support": [255, 255, 0, 255],
|
||||
|
||||
"layerview_infill": [0, 255, 255, 255],
|
||||
"layerview_support_infill": [0, 200, 200, 255],
|
||||
|
||||
"layerview_move_retraction": [0, 100, 255, 255]
|
||||
}
|
||||
}
|
||||
{"metadata": {"name": "Colorblind Assist Dark", "inherits": "cura-dark"}, "colors": {"x_axis": [212, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [126, 196, 193, 255]}}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,29 +1 @@
|
|||
{
|
||||
"metadata": {
|
||||
"name": "Colorblind Assist Light",
|
||||
"inherits": "cura-light"
|
||||
},
|
||||
|
||||
"colors": {
|
||||
|
||||
"x_axis": [200, 0, 0, 255],
|
||||
"y_axis": [64, 64, 255, 255],
|
||||
"model_overhang": [200, 0, 255, 255],
|
||||
"model_selection_outline": [12, 169, 227, 255],
|
||||
|
||||
"xray_error_dark": [255, 0, 0, 255],
|
||||
"xray_error_light": [255, 255, 0, 255],
|
||||
"xray": [26, 26, 62, 255],
|
||||
"xray_error": [255, 0, 0, 255],
|
||||
|
||||
"layerview_inset_0": [255, 64, 0, 255],
|
||||
"layerview_inset_x": [0, 156, 128, 255],
|
||||
"layerview_skin": [255, 255, 86, 255],
|
||||
"layerview_support": [255, 255, 0, 255],
|
||||
|
||||
"layerview_infill": [0, 255, 255, 255],
|
||||
"layerview_support_infill": [0, 200, 200, 255],
|
||||
|
||||
"layerview_move_retraction": [0, 100, 255, 255]
|
||||
}
|
||||
}
|
||||
{"metadata": {"name": "Colorblind Assist Light", "inherits": "cura-light"}, "colors": {"x_axis": [200, 0, 0, 255], "y_axis": [64, 64, 255, 255], "model_overhang": [200, 0, 255, 255], "model_selection_outline": [12, 169, 227, 255], "xray_error_dark": [255, 0, 0, 255], "xray_error_light": [255, 255, 0, 255], "xray": [26, 26, 62, 255], "xray_error": [255, 0, 0, 255], "layerview_inset_0": [255, 64, 0, 255], "layerview_inset_x": [0, 156, 128, 255], "layerview_skin": [255, 255, 86, 255], "layerview_support": [255, 255, 0, 255], "layerview_infill": [0, 255, 255, 255], "layerview_support_infill": [0, 200, 200, 255], "layerview_move_retraction": [0, 100, 255, 255], "main_window_header_background": [126, 196, 193, 255]}}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,16 +0,0 @@
|
|||
[
|
||||
[ 62, 33, 55, 255],
|
||||
[126, 196, 193, 255],
|
||||
[126, 196, 193, 255],
|
||||
[215, 155, 125, 255],
|
||||
[228, 148, 58, 255],
|
||||
[192, 199, 65, 255],
|
||||
[157, 48, 59, 255],
|
||||
[140, 143, 174, 255],
|
||||
[ 23, 67, 75, 255],
|
||||
[ 23, 67, 75, 255],
|
||||
[154, 99, 72, 255],
|
||||
[112, 55, 127, 255],
|
||||
[100, 125, 52, 255],
|
||||
[210, 100, 113, 255]
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue