This commit is contained in:
Simon 2025-12-11 09:12:22 +01:00 committed by GitHub
commit 79ab47ec79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1397 additions and 1180 deletions

View 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

View file

@ -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" }
}
}
}
}

View file

@ -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"
}
}

View file

@ -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)

View 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()

View file

@ -0,0 +1 @@
from .controller import Controller

View 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"

View 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))

View 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):
...

View 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()

View 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):
...

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,009 KiB

View 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 ##

View file

@ -0,0 +1,8 @@
from application import PrinterApplication
def main():
app = PrinterApplication()
app.run()
if __name__ == "__main__":
main()

View file

@ -0,0 +1 @@
from .tkinter_ui import TkinterUi as Ui

View 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

View 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)

View 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')])

View 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)

View 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)

View 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)

View 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)

View 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)

View file

@ -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 {}

View file

@ -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

View file

@ -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

View file

@ -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]
]