diff --git a/create_laser_parameters_files.sh b/create_laser_parameters_files.sh
new file mode 100755
index 00000000..609bc351
--- /dev/null
+++ b/create_laser_parameters_files.sh
@@ -0,0 +1,205 @@
+#!/bin/bash
+
+# Ensure the directory exists
+mkdir -p src/ui/laser_parameters
+
+# Create __init__.py (empty file)
+touch src/ui/laser_parameters/__init__.py
+
+# Create laser_parameters_dialog.py
+cat > src/ui/laser_parameters/laser_parameters_dialog.py << 'EOF'
+from PyQt5 import uic, QtWidgets
+import os
+import yaml
+
+class LaserParametersDialog(QtWidgets.QDialog):
+ def __init__(self, scancard, parent=None):
+ super().__init__(parent)
+
+ # Load UI file
+ ui_path = os.path.join(os.path.dirname(__file__), 'laser_parameters_dialog.ui')
+ uic.loadUi(ui_path, self)
+
+ self.scancard = scancard
+
+ # Connect buttons
+ self.buttonBox.accepted.connect(self.save_parameters)
+ self.buttonBox.rejected.connect(self.reject)
+
+ # Load existing parameters
+ self.load_parameters()
+
+ def load_parameters(self):
+ """Load parameters from YAML or set defaults."""
+ try:
+ with open('laser_parameters.yaml', 'r') as file:
+ params = yaml.safe_load(file)
+
+ # Load Marking Parameters
+ self.markSpeedSpinBox.setValue(float(params.get('mark_speed', 3000)))
+ # Add more parameter loading here
+
+ except FileNotFoundError:
+ self.reset_to_defaults()
+
+ def reset_to_defaults(self):
+ """Set default parameter values."""
+ self.markSpeedSpinBox.setValue(3000)
+ # Add more default parameter settings here
+
+ def save_parameters(self):
+ """Save parameters to YAML and update Scancard."""
+ # Collect marking parameters
+ marking_params = {
+ 'mark_speed': self.markSpeedSpinBox.value(),
+ # Add more parameter collection here
+ }
+
+ # Save to YAML
+ with open('laser_parameters.yaml', 'w') as file:
+ yaml.dump(marking_params, file)
+
+ # Update Scancard parameters
+ self.update_scancard_parameters(marking_params)
+
+ self.accept()
+
+ def update_scancard_parameters(self, marking_params):
+ """Update Scancard with marking parameters."""
+ try:
+ # Update marking parameters
+ self.scancard.set_markParameters_by_layer(0, {
+ 'markSpeed': marking_params['mark_speed'],
+ # Add more parameter updates here
+ })
+
+ except Exception as e:
+ QtWidgets.QMessageBox.warning(
+ self,
+ "Parameter Update Error",
+ f"Failed to update Scancard parameters: {str(e)}"
+ )
+EOF
+
+# Create laser_parameters_dialog.ui
+cat > src/ui/laser_parameters/laser_parameters_dialog.ui << 'EOF'
+
+
+ LaserParametersDialog
+
+
+
+ 0
+ 0
+ 700
+ 900
+
+
+
+ Laser Marking Parameters
+
+
+ -
+
+
+
+ Marking Parameters
+
+
+
-
+
+
+ Marking Parameters
+
+
+
-
+
+
+ Marking Speed (mm/s):
+
+
+
+ -
+
+
+ 10000.000000000000000
+
+
+
+
+
+
+
+
+
+
+ Fill Parameters
+
+
+ -
+
+
+ Entity Fill Properties
+
+
+
-
+
+
+ Fill Mode:
+
+
+
+ -
+
+
-
+
+ No Filling
+
+
+ -
+
+ One-way Filling
+
+
+ -
+
+ Two-way Filling
+
+
+ -
+
+ Bow-shaped Filling
+
+
+ -
+
+ Back-shaped Filling
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Save
+
+
+
+
+
+
+
+
+EOF
+
+echo "Files created successfully in src/ui/laser_parameters/"
\ No newline at end of file
diff --git a/src/Feeltek/scanCard.py b/src/Feeltek/scanCard.py
index 20dff983..8bb7ee38 100644
--- a/src/Feeltek/scanCard.py
+++ b/src/Feeltek/scanCard.py
@@ -2,7 +2,7 @@
import socket
import json
import time
-from typing import Optional, Dict, Any
+from typing import Optional, Dict, Any, List
from concurrent.futures import ThreadPoolExecutor, Future
from PyQt5.QtCore import QMutex
@@ -127,6 +127,7 @@ def __init__(self, parent=None):
self.timeout = 5
self.file = ""
self.ret_value = 1
+ self.current_file = None
self.req = {}
self.function = ""
@@ -137,6 +138,10 @@ def __init__(self, parent=None):
self.executor = ThreadPoolExecutor(max_workers=1)
self.mutex = QMutex()
+ # Track file queues for multi-layer printing
+ self.file_queue = []
+ self.current_file_index = -1
+
except Exception as e:
print(f"E1: Variable initialization failed. {e}")
@@ -180,20 +185,50 @@ def create_request(self, cmd: str, data: Optional[Dict[str, Any]] = None):
if data:
self.req["data"] = data
- def execute_command(self, cmd: str, data: Optional[Dict[str, Any]] = None) -> Future:
+ def execute_command(self, cmd: str, data: Optional[Dict[str, Any]] = None, retries=3, retry_delay=1.0) -> Future:
def task():
- try:
- json_string = json.dumps({"sid": 0, "cmd": cmd, "data": data})
- with socket.create_connection((self.HOST, self.PORT), timeout=self.timeout) as sock:
- sock.sendall(json_string.encode())
- ret = sock.recv(1024)
- if ret:
- response_data = json.loads(ret.decode('GB18030', errors='replace'))
- return {"ret_value": response_data.get("ret")}
- else:
- return {"ret_value": -1} # Simulated error response
- except (socket.timeout, socket.error, json.JSONDecodeError) as e:
- return {"ret_value": -1} # Simulated error response
+ attempts = 0
+ while attempts < retries:
+ try:
+ json_string = json.dumps({"sid": 0, "cmd": cmd, "data": data} if data else {"sid": 0, "cmd": cmd})
+ with socket.create_connection((self.HOST, self.PORT), timeout=self.timeout) as sock:
+ sock.sendall(json_string.encode())
+ ret = sock.recv(1024)
+ if ret:
+ ret_decoded = ret.decode('GB18030', errors='replace')
+ json_end_index = ret_decoded.rfind('}') + 1
+ json_content = ret_decoded[:json_end_index]
+ response_data = json.loads(json_content)
+ self.log_info(f"Command {cmd} executed successfully")
+ return {"ret_value": response_data.get("ret"), "response": response_data}
+ else:
+ self.log_error(f"No response received for command: {cmd}")
+ attempts += 1
+ if attempts < retries:
+ time.sleep(retry_delay)
+ self.log_info(f"Retrying command {cmd}, attempt {attempts+1}/{retries}")
+ continue
+ except (socket.timeout, socket.error) as e:
+ self.log_error(f"Socket error executing command {cmd}: {e}")
+ attempts += 1
+ if attempts < retries:
+ time.sleep(retry_delay)
+ self.log_info(f"Retrying command {cmd}, attempt {attempts+1}/{retries}")
+ continue
+ except json.JSONDecodeError as e:
+ self.log_error(f"JSON decode error executing command {cmd}: {e}")
+ attempts += 1
+ if attempts < retries:
+ time.sleep(retry_delay)
+ self.log_info(f"Retrying command {cmd}, attempt {attempts+1}/{retries}")
+ continue
+ except Exception as e:
+ self.log_error(f"Unexpected error executing command {cmd}: {e}")
+ break
+
+ # If we've reached this point, all retries failed
+ self.log_error(f"Command {cmd} failed after {retries} attempts")
+ return {"ret_value": -1, "error": "Command failed after retries"}
self.mutex.lock()
future = self.executor.submit(task)
@@ -240,119 +275,323 @@ def task():
return future
def open_file(self, file_path: str):
+ """Open a file on the scancard."""
+ self.current_file = file_path
return self.execute_command("open_file", {"path": file_path})
def close_file(self):
+ """Close the current file."""
+ self.current_file = None
return self.execute_command("close_file")
def save_file(self, file_path: str, cover: bool):
- return self.execute_command("save_file", {"path": file_path, "cover": cover})
+ """Save a file on the scancard."""
+ return self.execute_command("save_file", {"path": file_path, "cover": 1 if cover else 0})
def start_mark(self):
+ """Start the marking process."""
future = self.execute_command("start_mark")
return future
def stop_mark(self):
+ """Stop the marking process."""
future = self.execute_command("stop_mark")
return future
def start_preview(self):
+ """Start preview mode."""
return self.execute_command("start_preview")
def stop_preview(self):
+ """Stop preview mode."""
return self.execute_command("stop_preview")
+ def set_file_queue(self, file_paths: List[str]):
+ """Set a queue of files to be processed in sequence."""
+ self.file_queue = file_paths
+ self.current_file_index = -1
+
+ def load_next_file(self) -> Future:
+ """Load the next file in the queue."""
+ if not self.file_queue or self.current_file_index >= len(self.file_queue) - 1:
+ return None
+
+ self.current_file_index += 1
+ file_path = self.file_queue[self.current_file_index]
+ return self.open_file(file_path)
+
+ def get_current_file_index(self) -> int:
+ """Get the current file index in the queue."""
+ return self.current_file_index
+
+ def get_file_queue_length(self) -> int:
+ """Get the length of the file queue."""
+ return len(self.file_queue)
+
def get_markParameters_by_layer(self, layer_id: int):
+ """Get marking parameters for a layer."""
return self.execute_command("get_markParameters_by_layer", {"layer_id": layer_id})
def set_markParameters_by_layer(self, layer_id: int, params: Dict[str, Any]):
- return self.execute_command("set_markParameters_by_layer", {"layer_id": layer_id, **params})
+ """Set marking parameters for a layer."""
+ data = {"layer_id": layer_id, **params}
+ return self.execute_command("set_markParameters_by_layer", data)
def get_markParameters_by_index(self, index: int, in_index: int):
+ """Get marking parameters by index."""
return self.execute_command("get_markParameters_by_index", {"index": index, "in_index": in_index})
def set_markParameters_by_index(self, index: int, in_index: int, params: Dict[str, Any]):
- return self.execute_command("set_markParameters_by_index", {"index": index, "in_index": in_index, **params})
+ """Set marking parameters by index."""
+ data = {"index": index, "in_index": in_index, **params}
+ return self.execute_command("set_markParameters_by_index", data)
def download_parameters(self):
+ """Download marking parameters."""
return self.execute_command("download_Parameters")
def get_entity_fill_property_by_index(self, index: int, in_index: int):
+ """Get fill properties by index."""
return self.execute_command("get_entity_fill_property_by_index", {"index": index, "in_index": in_index})
def set_entity_fill_property_by_index(self, index: int, in_index: int, params: Dict[str, Any]):
- return self.execute_command("set_entity_fill_property_by_index", {"index": index, "in_index": in_index, **params})
+ """Set fill properties by index."""
+ data = {"index": index, "in_index": in_index, **params}
+ return self.execute_command("set_entity_fill_property_by_index", data)
def get_entity_count(self):
+ """Get the number of entities."""
return self.execute_command("get_entity_count")
def translate_entity(self, dx: float, dy: float):
+ """Translate all entities."""
return self.execute_command("translate_entity", {"dx": dx, "dy": dy})
def rotate_entity(self, cx: float, cy: float, fAngle: float):
+ """Rotate all entities."""
return self.execute_command("rotate_entity", {"cx": cx, "cy": cy, "fAngle": fAngle})
def translate_entity_by_index(self, index: int, dx: float, dy: float):
+ """Translate entity by index."""
return self.execute_command("translate_entity_by_index", {"index": index, "dx": dx, "dy": dy})
def rotate_entity_by_index(self, index: int, cx: float, cy: float, fAngle: float):
+ """Rotate entity by index."""
return self.execute_command("rotate_entity_by_index", {"index": index, "cx": cx, "cy": cy, "fAngle": fAngle})
def trans_by_model(self, dx: float, dy: float, dz: float, axis: str, fAngle: float, fScale: float):
+ """Model transformation."""
return self.execute_command("TransByModel", {"dx": dx, "dy": dy, "dz": dz, "axis": axis, "fAngle": fAngle, "fScale": fScale})
def get_name_by_index(self, index: int):
+ """Get name by index."""
return self.execute_command("get_name_by_index", {"index": index})
def set_name_by_index(self, index: int, name: str):
+ """Set name by index."""
return self.execute_command("set_name_by_index", {"index": index, "name": name})
def get_content_by_index(self, index: int):
+ """Get content by index."""
return self.execute_command("get_content_by_index", {"index": index})
def set_content_by_index(self, index: int, content: str):
+ """Set content by index."""
return self.execute_command("set_content_by_index", {"index": index, "content": content})
def get_pos_size_by_index(self, index: int):
+ """Get position and size by index."""
return self.execute_command("get_pos_size_by_index", {"index": index})
def set_pos_size_by_index(self, index: int, xPos: float, yPos: float, zPos: float, xSize: float, ySize: float, zSize: float):
- return self.execute_command("set_pos_size_by_index", {"index": index, "xPos": xPos, "yPos": yPos, "zPos": zPos, "xSize": xSize, "ySize": ySize, "zSize": zSize})
+ """Set position and size by index."""
+ return self.execute_command("set_pos_size_by_index", {
+ "index": index,
+ "xPos": xPos,
+ "yPos": yPos,
+ "zPos": zPos,
+ "xSize": xSize,
+ "ySize": ySize,
+ "zSize": zSize
+ })
def get_content_by_name(self, name: str):
+ """Get content by name."""
return self.execute_command("get_content_by_name", {"name": name})
def set_content_by_name(self, name: str, content: str):
+ """Set content by name."""
+ return self
+ def set_content_by_name(self, name: str, content: str):
+ """Set content by name."""
return self.execute_command("set_content_by_name", {"name": name, "content": content})
def delete_by_index(self, index: int):
+ """Delete object by index."""
return self.execute_command("delete_by_index", {"index": index})
def copy_by_index(self, index: int):
+ """Copy object by index."""
return self.execute_command("copy_by_index", {"index": index})
def mark_by_index(self, index: int):
+ """Mark object by index."""
return self.execute_command("mark_by_index", {"index": index})
def read_input(self):
+ """Read input pins."""
return self.execute_command("read_input", {"data": 0xff})
def set_output(self, output: int):
+ """Set output pins."""
return self.execute_command("write_output", {"output": output})
def clear_error(self):
+ """Clear current error."""
return self.execute_command("clear_error")
def get_error(self):
+ """Get current error."""
future = self.execute_command("get_error")
future.add_done_callback(lambda f: self.log_info(f"Error description: {self.ERROR_DESCRIPTIONS.get(self.ret_value, 'Unknown error')}"))
return future
def enable_vision(self, bEnVision: bool):
+ """Enable or disable vision system."""
return self.execute_command("enable_vision", {"bEnVision": bEnVision})
def vision_translate(self, dX: float, dY: float):
+ """Translate using vision system."""
return self.execute_command("vision_translate", {"dX": dX, "dY": dY})
def vision_rotate(self, cX: float, cY: float, fAngle: float):
- return self.execute_command("vision_rotate", {"cX": cX, "cY": cY, "fAngle": fAngle})
\ No newline at end of file
+ """Rotate using vision system."""
+ return self.execute_command("vision_rotate", {"cX": cX, "cY": cY, "fAngle": fAngle})
+
+ # Add this helper method to the Scancard class in src/Feeltek/scanCard.py
+
+ def get_max_layer_count(self):
+ """
+ Gets the maximum number of layers in the current job.
+ Uses get_entity_count as a proxy for the total number of layers.
+
+ Returns:
+ int: The maximum layer count in the current job, or 1 if not determinable
+ """
+ try:
+ future = self.get_entity_count()
+ response = future.result()
+
+ if response and response.get("ret_value") == 1:
+ # Get the count from the response data
+ count = response.get("response", {}).get("data", {}).get("count", 1)
+ return max(1, count) # Ensure at least 1 layer
+ else:
+ return 1 # Default to 1 layer if we can't determine
+ except Exception as e:
+ print(f"Error getting max layer count: {e}")
+ return 1 # Default to 1 layer on error
+
+ def validate_layer_entity(self, layer_id, entity_index=1, entity_subindex=1) -> Future:
+ """
+ Validates that a specific layer and entity exists.
+
+ Args:
+ layer_id: The layer ID to validate
+ entity_index: The entity index to validate
+ entity_subindex: The entity subindex to validate
+
+ Returns:
+ Future with result containing:
+ - valid: True if layer and entity exist, False otherwise
+ - message: Description of any validation issues
+ """
+ def task():
+ # First check if we can get marking parameters for this layer
+ try:
+ mark_params_future = self.get_markParameters_by_layer(layer_id)
+ mark_params_result = mark_params_future.result()
+
+ if not mark_params_result or mark_params_result.get("ret_value") != 1:
+ return {
+ "valid": False,
+ "message": f"Layer {layer_id} does not exist or cannot be accessed"
+ }
+
+ # Next check if we can get fill properties for this entity
+ fill_props_future = self.get_entity_fill_property_by_index(entity_index, entity_subindex)
+ fill_props_result = fill_props_future.result()
+
+ if not fill_props_result or fill_props_result.get("ret_value") != 1:
+ return {
+ "valid": False,
+ "message": f"Entity at index {entity_index},{entity_subindex} does not exist"
+ }
+
+ # Both checks passed
+ return {"valid": True, "message": "Layer and entity validated"}
+
+ except Exception as e:
+ self.log_error(f"Error validating layer {layer_id}, entity {entity_index},{entity_subindex}: {e}")
+ return {"valid": False, "message": f"Validation error: {str(e)}"}
+
+ self.mutex.lock()
+ future = self.executor.submit(task)
+ future.add_done_callback(lambda f: self.mutex.unlock())
+ return future
+
+ def process_multiple_files(self, file_paths: List[str], callback=None):
+ """Process multiple files in sequence.
+
+ Args:
+ file_paths: List of file paths to process
+ callback: Optional callback function to call after each file is processed
+
+ Returns:
+ Future object that will be completed when all files are processed
+ """
+ def task():
+ results = []
+ for i, file_path in enumerate(file_paths):
+ # Close any open file
+ close_result = self.close_file().result()
+
+ # Open new file
+ open_result = self.open_file(file_path).result()
+ if open_result.get("ret_value") != 1:
+ results.append({
+ "file": file_path,
+ "success": False,
+ "error": f"Failed to open file: {self.ERROR_DESCRIPTIONS.get(open_result.get('ret_value'), 'Unknown error')}"
+ })
+ continue
+
+ # Mark the file
+ mark_result = self.start_mark().result()
+ if mark_result.get("ret_value") != 1:
+ results.append({
+ "file": file_path,
+ "success": False,
+ "error": f"Failed to mark file: {self.ERROR_DESCRIPTIONS.get(mark_result.get('ret_value'), 'Unknown error')}"
+ })
+ continue
+
+ # Wait for marking to complete
+ while True:
+ status = self.get_working_status().result()
+ if status == "Waiting":
+ break
+ time.sleep(0.5)
+
+ results.append({
+ "file": file_path,
+ "success": True
+ })
+
+ # Call callback if provided
+ if callback:
+ callback(i, len(file_paths), file_path)
+
+ return results
+
+ return self.executor.submit(task)
\ No newline at end of file
diff --git a/src/config.py b/src/config.py
index 4594b90b..cc2df368 100644
--- a/src/config.py
+++ b/src/config.py
@@ -1,2 +1,2 @@
class Config:
- DEVELOPMENT_MODE = False # Set to False in production
\ No newline at end of file
+ DEVELOPMENT_MODE = True # Set to False in production
\ No newline at end of file
diff --git a/src/layerManager/layerQueueManager.py b/src/layerManager/layerQueueManager.py
new file mode 100644
index 00000000..581588b1
--- /dev/null
+++ b/src/layerManager/layerQueueManager.py
@@ -0,0 +1,157 @@
+import os
+import re
+import logging
+
+class LayerQueueManager:
+ """
+ Manages the queue of layer files for multi-layer printing.
+ Handles loading, sorting, and tracking of layer files.
+ """
+
+ def __init__(self):
+ """Initialize the layer queue manager."""
+ self.layer_files = []
+ self.current_layer_index = -1
+ self.total_layers = 0
+
+ # Configure logging
+ self.logger = logging.getLogger(__name__)
+ formatter = logging.Formatter(
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+ )
+ handler = logging.StreamHandler()
+ handler.setFormatter(formatter)
+ self.logger.addHandler(handler)
+ self.logger.setLevel(logging.INFO)
+
+ def load_from_folder(self, folder_path):
+ """
+ Load all layer files from a folder.
+
+ Args:
+ folder_path (str): Path to the folder containing layer files
+
+ Returns:
+ list: Sorted list of file paths
+ """
+ self.logger.info(f"Loading layer files from folder: {folder_path}")
+ self.layer_files = []
+ valid_extensions = ['.emd'] # Feeltek/LenMark file format
+ layer_file_tuples = []
+
+ try:
+ for file in os.listdir(folder_path):
+ if any(file.lower().endswith(ext) for ext in valid_extensions):
+ # Extract layer number from filename if available
+ match = re.search(r'layer[_-]?(\d+)', file.lower())
+ file_path = os.path.join(folder_path, file)
+
+ if match:
+ layer_num = int(match.group(1))
+ self.logger.debug(f"Found layer file {file} with layer number {layer_num}")
+ layer_file_tuples.append((layer_num, file_path))
+ else:
+ # If no layer number in filename, use modified time
+ mod_time = os.path.getmtime(file_path)
+ self.logger.debug(f"Found layer file {file} with modified time {mod_time}")
+ layer_file_tuples.append((mod_time, file_path))
+ except Exception as e:
+ self.logger.error(f"Error loading files from folder: {e}")
+ return []
+
+ # Sort by layer number or modified time
+ layer_file_tuples.sort(key=lambda x: x[0])
+
+ # Store just the file paths in order
+ self.layer_files = [file_path for _, file_path in layer_file_tuples]
+ self.total_layers = len(self.layer_files)
+ self.current_layer_index = -1
+
+ self.logger.info(f"Loaded {self.total_layers} layer files")
+ return self.layer_files
+
+ def get_current_layer(self):
+ """
+ Get the current layer file.
+
+ Returns:
+ str: Path to the current layer file, or None if no current layer
+ """
+ if 0 <= self.current_layer_index < self.total_layers:
+ return self.layer_files[self.current_layer_index]
+ return None
+
+ def get_next_layer(self):
+ """
+ Advance to the next layer and return its file path.
+
+ Returns:
+ str: Path to the next layer file, or None if no more layers
+ """
+ if self.current_layer_index + 1 < self.total_layers:
+ self.current_layer_index += 1
+ self.logger.info(f"Advancing to layer {self.current_layer_index + 1} of {self.total_layers}")
+ return self.layer_files[self.current_layer_index]
+
+ self.logger.info("No more layers available")
+ return None
+
+ def peek_next_layer(self):
+ """
+ Look at the next layer without advancing.
+
+ Returns:
+ str: Path to the next layer file, or None if no more layers
+ """
+ if self.current_layer_index + 1 < self.total_layers:
+ return self.layer_files[self.current_layer_index + 1]
+ return None
+
+ def reset(self):
+ """Reset to the beginning of the queue."""
+ self.current_layer_index = -1
+ self.logger.info("Layer queue reset")
+
+ def set_current_layer_index(self, index):
+ """
+ Set the current layer index to a specific value.
+ Useful for resuming from a saved state.
+
+ Args:
+ index (int): The index to set as current
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ if 0 <= index < self.total_layers:
+ self.current_layer_index = index
+ self.logger.info(f"Current layer index set to {index}")
+ return True
+
+ self.logger.error(f"Invalid layer index: {index}")
+ return False
+
+ def progress_percentage(self):
+ """
+ Calculate the current progress as a percentage.
+
+ Returns:
+ int: Progress percentage (0-100)
+ """
+ if self.total_layers == 0:
+ return 0
+
+ if self.current_layer_index < 0:
+ return 0
+
+ return int((self.current_layer_index + 1) / self.total_layers * 100)
+
+ def get_layer_count(self):
+ """
+ Get the total number of layers/files in the queue.
+
+ Returns:
+ int: The total number of layers
+ """
+ return self.total_layers
\ No newline at end of file
diff --git a/src/layerManager/printStateManager.py b/src/layerManager/printStateManager.py
new file mode 100644
index 00000000..d1ee1527
--- /dev/null
+++ b/src/layerManager/printStateManager.py
@@ -0,0 +1,154 @@
+import json
+import os
+import time
+import logging
+from datetime import datetime
+
+class PrintStateManager:
+ """
+ Manages the saving and loading of print states to enable resumable printing.
+ """
+
+ def __init__(self, state_dir="print_states"):
+ """
+ Initialize the print state manager.
+
+ Args:
+ state_dir (str): Directory to store print state files
+ """
+ self.state_dir = state_dir
+ os.makedirs(state_dir, exist_ok=True)
+
+ # Configure logging
+ self.logger = logging.getLogger(__name__)
+ formatter = logging.Formatter(
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+ )
+ handler = logging.StreamHandler()
+ handler.setFormatter(formatter)
+ self.logger.addHandler(handler)
+ self.logger.setLevel(logging.INFO)
+
+ def save_print_state(self, state):
+ """
+ Save the current print state to a file.
+
+ Args:
+ state (dict): The print state to save, containing:
+ - current_layer_index: Index of the current layer
+ - total_layers: Total number of layers
+ - layer_files: List of layer file paths
+ - timestamp: When the state was saved (added automatically)
+
+ Returns:
+ str: Path to the saved state file
+ """
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f"print_state_{timestamp}.pstate"
+ filepath = os.path.join(self.state_dir, filename)
+
+ # Add timestamp to state
+ state_with_timestamp = state.copy()
+ state_with_timestamp['timestamp'] = timestamp
+ state_with_timestamp['save_time'] = time.time()
+
+ try:
+ with open(filepath, 'w') as f:
+ json.dump(state_with_timestamp, f, indent=2)
+
+ self.logger.info(f"Print state saved to {filepath}")
+ return filepath
+ except Exception as e:
+ self.logger.error(f"Error saving print state: {e}")
+ return None
+
+ def load_print_state(self, state_file):
+ """
+ Load a print state from a file.
+
+ Args:
+ state_file (str): Path to the state file to load
+
+ Returns:
+ dict: The loaded state, or None if loading failed
+ """
+ try:
+ with open(state_file, 'r') as f:
+ state = json.load(f)
+
+ # Validate that the necessary keys are present
+ required_keys = ['current_layer_index', 'total_layers', 'layer_files']
+ if not all(key in state for key in required_keys):
+ self.logger.error(f"Invalid state file: missing required keys")
+ return None
+
+ self.logger.info(f"Loaded print state from {state_file}")
+ return state
+ except Exception as e:
+ self.logger.error(f"Error loading print state: {e}")
+ return None
+
+ def list_saved_states(self):
+ """
+ List all saved print states.
+
+ Returns:
+ list: List of dictionaries containing state information:
+ - file_path: Path to the state file
+ - timestamp: When the state was saved
+ - current_layer: Current layer index
+ - total_layers: Total number of layers
+ """
+ state_files = []
+
+ try:
+ for file in os.listdir(self.state_dir):
+ if file.endswith(".pstate"):
+ file_path = os.path.join(self.state_dir, file)
+
+ try:
+ with open(file_path, 'r') as f:
+ state = json.load(f)
+
+ state_info = {
+ 'file_path': file_path,
+ 'timestamp': state.get('timestamp', 'Unknown'),
+ 'current_layer': state.get('current_layer_index', -1) + 1,
+ 'total_layers': state.get('total_layers', 0),
+ 'save_time': state.get('save_time', 0)
+ }
+
+ state_files.append(state_info)
+ except Exception as e:
+ self.logger.warning(f"Error reading state file {file}: {e}")
+
+ # Sort by save time, newest first
+ state_files.sort(key=lambda x: x['save_time'], reverse=True)
+
+ return state_files
+ except Exception as e:
+ self.logger.error(f"Error listing saved states: {e}")
+ return []
+
+ def delete_state_file(self, state_file):
+ """
+ Delete a state file.
+
+ Args:
+ state_file (str): Path to the state file to delete
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ try:
+ if os.path.exists(state_file):
+ os.remove(state_file)
+ self.logger.info(f"Deleted state file {state_file}")
+ return True
+ else:
+ self.logger.warning(f"State file not found: {state_file}")
+ return False
+ except Exception as e:
+ self.logger.error(f"Error deleting state file: {e}")
+ return False
\ No newline at end of file
diff --git a/src/multiLayerPrintController.py b/src/multiLayerPrintController.py
new file mode 100644
index 00000000..c991ab21
--- /dev/null
+++ b/src/multiLayerPrintController.py
@@ -0,0 +1,265 @@
+import os
+import time
+import logging
+from concurrent.futures import Future
+from PyQt5.QtCore import QObject, pyqtSignal
+
+from layerManager.layerQueueManager import LayerQueueManager
+from layerManager.printStateManager import PrintStateManager
+from utils.helpers import run_async
+
+class MultiLayerPrintController(QObject):
+ """
+ Controller for managing multi-layer printing process.
+ Coordinates the layer queue, print state, and automation processes.
+ """
+
+ # Signals
+ progress_update_signal = pyqtSignal(int)
+ status_update_signal = pyqtSignal(str)
+ layer_changed_signal = pyqtSignal(int, str) # layer_index, layer_file
+ print_completed_signal = pyqtSignal()
+ print_paused_signal = pyqtSignal()
+ print_resumed_signal = pyqtSignal()
+ print_aborted_signal = pyqtSignal(str) # reason
+
+ def __init__(self, main_window):
+ """
+ Initialize the multi-layer print controller.
+
+ Args:
+ main_window: The main application window
+ """
+ super().__init__()
+ self.main_window = main_window
+ self.layer_manager = LayerQueueManager()
+ self.state_manager = PrintStateManager()
+
+ # Get references to necessary components
+ self.process_automation = main_window.process_automation_controller
+ self.scancard = main_window.scancard
+
+ # State variables
+ self.print_in_progress = False
+ self.paused = False
+ self.aborted = False
+ self.current_operation = "idle"
+ self.current_state_file = None
+
+ # Configure logging
+ self.logger = logging.getLogger(__name__)
+ formatter = logging.Formatter(
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+ )
+ handler = logging.StreamHandler()
+ handler.setFormatter(formatter)
+ self.logger.addHandler(handler)
+ self.logger.setLevel(logging.INFO)
+
+ def load_layer_files(self, folder_path):
+ """
+ Load layer files from a folder.
+
+ Args:
+ folder_path (str): Path to the folder containing layer files
+
+ Returns:
+ list: List of layer file paths
+ """
+ self.logger.info(f"Loading layer files from folder: {folder_path}")
+ layer_files = self.layer_manager.load_from_folder(folder_path)
+ return layer_files
+
+ @run_async
+ def start_multi_layer_print(self):
+ """
+ Start the multi-layer print process.
+ """
+ if len(self.layer_manager.layer_files) == 0:
+ self.logger.error("No layer files loaded")
+ self.print_aborted_signal.emit("No layer files loaded")
+ return
+
+ if self.print_in_progress:
+ self.logger.warning("Print already in progress")
+ return
+
+ self.print_in_progress = True
+ self.paused = False
+ self.aborted = False
+
+ # Reset layer manager to start from the beginning
+ self.layer_manager.reset()
+
+ # Save initial state
+ self._save_current_state()
+
+ self.logger.info("Starting multi-layer print process")
+ self.status_update_signal.emit("Starting print process")
+
+ try:
+ # Initial setup
+ yield from self._perform_initial_setup()
+
+ if self.aborted:
+ self.logger.info("Print aborted during initial setup")
+ self.print_aborted_signal.emit("Aborted during initial setup")
+ return
+
+ # Process each layer
+ while self.layer_manager.get_next_layer() is not None:
+ if self.aborted:
+ self.logger.info("Print aborted")
+ self.print_aborted_signal.emit("Aborted by user")
+ return
+
+ # Handle pause state
+ yield from self._handle_pause_state()
+
+ if self.aborted:
+ self.logger.info("Print aborted during pause")
+ self.print_aborted_signal.emit("Aborted during pause")
+ return
+
+ # Process the current layer
+ current_layer = self.layer_manager.get_current_layer()
+ current_index = self.layer_manager.current_layer_index
+
+ # Update UI
+ self.progress_update_signal.emit(self.layer_manager.progress_percentage())
+ self.layer_changed_signal.emit(current_index, current_layer)
+
+ # Process the layer
+ yield from self._process_layer(current_layer)
+
+ # Save state after each layer
+ self._save_current_state()
+
+ # Finalize print
+ if not self.aborted:
+ yield from self._finalize_print()
+ self.print_completed_signal.emit()
+
+ except Exception as e:
+ self.logger.error(f"Error in multi-layer print: {e}", exc_info=True)
+ self.print_aborted_signal.emit(f"Error: {str(e)}")
+
+ finally:
+ self.print_in_progress = False
+ self.current_operation = "idle"
+ self.status_update_signal.emit("Print process ended")
+
+ @run_async
+ def resume_multi_layer_print(self, state_file=None):
+ """
+ Resume printing from a saved state.
+
+ Args:
+ state_file (str, optional): Path to the state file to resume from.
+ If None, uses the most recently saved state.
+ """
+ # If no state file provided, use the current one
+ if state_file is None:
+ state_file = self.current_state_file
+
+ if state_file is None:
+ saved_states = self.state_manager.list_saved_states()
+ if not saved_states:
+ self.logger.error("No saved states found to resume from")
+ self.print_aborted_signal.emit("No saved states found")
+ return
+
+ # Use the most recent state
+ state_file = saved_states[0]['file_path']
+
+ # Load the state
+ state = self.state_manager.load_print_state(state_file)
+ if not state:
+ self.logger.error(f"Failed to load state from {state_file}")
+ self.print_aborted_signal.emit("Failed to load state")
+ return
+
+ # Restore the state
+ self.layer_manager.layer_files = state['layer_files']
+ self.layer_manager.total_layers = state['total_layers']
+ current_layer_index = state['current_layer_index']
+
+ # Set the current layer
+ self.layer_manager.set_current_layer_index(current_layer_index)
+
+ # Start/resume the print
+ self.current_state_file = state_file
+ self.print_in_progress = True
+ self.paused = False
+ self.aborted = False
+
+ self.logger.info(f"Resuming print from layer {current_layer_index + 1} of {self.layer_manager.total_layers}")
+ self.status_update_signal.emit(f"Resuming print from layer {current_layer_index + 1}")
+ self.print_resumed_signal.emit()
+
+ try:
+ # Update UI
+ self.progress_update_signal.emit(self.layer_manager.progress_percentage())
+ current_layer = self.layer_manager.get_current_layer()
+ if current_layer:
+ self.layer_changed_signal.emit(current_layer_index, current_layer)
+
+ # Process each remaining layer
+ while self.layer_manager.get_next_layer() is not None:
+ if self.aborted:
+ self.logger.info("Print aborted")
+ self.print_aborted_signal.emit("Aborted by user")
+ return
+
+ # Handle pause state
+ yield from self._handle_pause_state()
+
+ if self.aborted:
+ self.logger.info("Print aborted during pause")
+ self.print_aborted_signal.emit("Aborted during pause")
+ return
+
+ # Process the current layer
+ current_layer = self.layer_manager.get_current_layer()
+ current_index = self.layer_manager.current_layer_index
+
+ # Update UI
+ self.progress_update_signal.emit(self.layer_manager.progress_percentage())
+ self.layer_changed_signal.emit(current_index, current_layer)
+
+ # Process the layer
+ yield from self._process_layer(current_layer)
+
+ # Save state after each layer
+ self._save_current_state()
+
+ # Finalize print
+ if not self.aborted:
+ yield from self._finalize_print()
+ self.print_completed_signal.emit()
+
+ except Exception as e:
+ self.logger.error(f"Error in resuming multi-layer print: {e}", exc_info=True)
+ self.print_aborted_signal.emit(f"Error: {str(e)}")
+
+ finally:
+ self.print_in_progress = False
+ self.current_operation = "idle"
+ self.status_update_signal.emit("Print process ended")
+
+ def pause_print(self):
+ """Pause the print process."""
+ if self.print_in_progress and not self.paused:
+ self.paused = True
+ self.logger.info("Print paused")
+ self.status_update_signal.emit("Print paused")
+ self.print_paused_signal.emit()
+
+ def resume_print(self):
+ """Resume a paused print."""
+ if self.print_in_progress and self.paused:
+ self.paused = False
+ self.logger.info("Print resumed")
+ self.status_update_signal.emit("Print resumed")
+ self.print_resumed_signal.emit()
\ No newline at end of file
diff --git a/src/processAutomationController/processAutomationController.py b/src/processAutomationController/processAutomationController.py
index 3e406d71..13508f01 100644
--- a/src/processAutomationController/processAutomationController.py
+++ b/src/processAutomationController/processAutomationController.py
@@ -1,8 +1,9 @@
from PyQt5.QtCore import QObject, pyqtSignal
from utils.helpers import run_async
import time
-
-# TBD clean play pause process. use printer printing status to diferentiate between control and main printing sequence
+import json
+import os
+from layerManager.printStateManager import PrintStateManager
class ProcessAutomationController(QObject):
progress_update_signal = pyqtSignal(int)
@@ -11,6 +12,10 @@ def __init__(self, main_window):
super(ProcessAutomationController, self).__init__()
self.main_window = main_window
self.process_running = False
+ self.print_state_manager = PrintStateManager()
+ self.current_layer_index = -1
+ self.total_layers = 0
+ self.layer_files = []
# Connect the progress update signal to the slot
self.progress_update_signal.connect(self.update_progress_bar)
@@ -199,6 +204,41 @@ def start_printing_sequence(self):
self.set_motion_control_buttons_enabled(True)
+ def process_layer(self, layer_file):
+ """Process a single layer file."""
+ # Open the layer file
+ future = self.main_window.scancard.open_file(layer_file)
+ future.result() # Wait for completion
+
+ # Print the layer
+ print(f"Processing layer: {os.path.basename(layer_file)}")
+
+ # Start marking
+ future = self.main_window.scancard.start_mark()
+ future.result() # Wait for completion
+
+ # Wait for marking to complete
+ self._wait_for_marking_complete()
+
+ # Recoat for the next layer
+ self.dose_recoat_layer()
+
+ # Save current state
+ self.print_state_manager.save_print_state({
+ 'current_layer_index': self.current_layer_index,
+ 'total_layers': self.total_layers,
+ 'layer_files': self.layer_files
+ })
+
+ def _wait_for_marking_complete(self):
+ """Wait for marking to complete."""
+ while True:
+ future = self.main_window.scancard.get_working_status()
+ status = future.result()
+ if status == "Waiting":
+ break
+ time.sleep(1) # Sleep for a short duration
+
def stop_process(self):
"""Stop the recoat process."""
self.process_running = False
@@ -210,22 +250,23 @@ def set_motion_control_buttons_enabled(self, enabled):
for button in self.main_window.control_screen.motion_control_buttons:
button.setEnabled(enabled)
+
def replace_placeholders(sequence: str, printer_status) -> str:
- """Replace placeholders in the sequence with actual values from the printer_status model."""
- placeholders = {
- "{layerHeight}": printer_status.layerHeight,
- "{initialLevellingHeight}": printer_status.initialLevellingHeight,
- "{heatedBufferHeight}": printer_status.heatedBufferHeight,
- "{powderLoadingExtraHeightGap}": printer_status.powderLoadingExtraHeightGap,
- "{bedTemperature}": printer_status.bedTemperature,
- "{volumeTemperature}": printer_status.volumeTemperature,
- "{chamberTemperature}": printer_status.chamberTemperature,
- "{p}": printer_status.p,
- "{i}": printer_status.i,
- "{d}": printer_status.d,
- "{powderLoadingHeight}": printer_status.initialLevellingHeight + 2 * printer_status.heatedBufferHeight + printer_status.partHeight,
- "{dosingHeight}": printer_status.dosingHeight # Add dosingHeight
- }
- for placeholder, value in placeholders.items():
- sequence = sequence.replace(placeholder, str(value))
- return sequence
\ No newline at end of file
+ """Replace placeholders in the sequence with actual values from the printer_status model."""
+ placeholders = {
+ "{layerHeight}": printer_status.layerHeight,
+ "{initialLevellingHeight}": printer_status.initialLevellingHeight,
+ "{heatedBufferHeight}": printer_status.heatedBufferHeight,
+ "{powderLoadingExtraHeightGap}": printer_status.powderLoadingExtraHeightGap,
+ "{bedTemperature}": printer_status.bedTemperature,
+ "{volumeTemperature}": printer_status.volumeTemperature,
+ "{chamberTemperature}": printer_status.chamberTemperature,
+ "{p}": printer_status.p,
+ "{i}": printer_status.i,
+ "{d}": printer_status.d,
+ "{powderLoadingHeight}": printer_status.initialLevellingHeight + 2 * printer_status.heatedBufferHeight + printer_status.partHeight,
+ "{dosingHeight}": printer_status.dosingHeight # Add dosingHeight
+ }
+ for placeholder, value in placeholders.items():
+ sequence = sequence.replace(placeholder, str(value))
+ return sequence
\ No newline at end of file
diff --git a/src/ui/control_screen/control_screen.py b/src/ui/control_screen/control_screen.py
index 70865000..e873edd4 100644
--- a/src/ui/control_screen/control_screen.py
+++ b/src/ui/control_screen/control_screen.py
@@ -1,7 +1,7 @@
-#TBD: incase a gcode is yet to be executed, block the thread from executing another gcode in moonraker api
-
from PyQt5 import uic
-from PyQt5.QtWidgets import (QWidget, QPushButton, QSpinBox, QProgressBar, QSizePolicy, QVBoxLayout, QMessageBox, QLabel)
+from PyQt5.QtWidgets import (QWidget, QPushButton, QSpinBox, QProgressBar, QSizePolicy,
+ QVBoxLayout, QMessageBox, QLabel, QFileDialog, QGroupBox,
+ QHBoxLayout, QListWidget)
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
from PyQt5.QtGui import QImage
import numpy as np
@@ -9,6 +9,8 @@
from utils.helpers import run_async
import time
from processAutomationController.processAutomationController import ProcessAutomationController
+from ui.layer_management.layer_queue_widget import LayerQueueWidget
+from ui.laser_parameters.laser_parameters_dialog import LaserParametersDialog
class ControlScreen(QWidget):
progress_update_signal = pyqtSignal(int)
@@ -23,6 +25,9 @@ def __init__(self, main_window):
# Initialize UI elements
self.initialize_ui_elements()
+ # Add Laser Parameters button
+ self.add_laser_parameters_button()
+
# Initialize ProcessAutomationController
self.process_automation_controller = ProcessAutomationController(main_window)
@@ -32,6 +37,9 @@ def __init__(self, main_window):
# Replace QWidget with custom ImageWidget
self.setup_custom_widgets()
+ # Setup multi-layer controls
+ self.setup_multi_layer_controls()
+
# Connect signals to slots
self.connect_signals()
@@ -49,9 +57,55 @@ def __init__(self, main_window):
# Connect the scancard status update signal to the label update slot
self.main_window.printer_status.scancard_status_updated.connect(self.update_laser_status)
+ def add_laser_parameters_button(self):
+ """Add Laser Parameters button to the control screen."""
+ # Find the right panel or create a new layout
+ right_panel = self.findChild(QWidget, "rightPanel")
+
+ # Create Laser Parameters Group Box
+ laser_params_group = QGroupBox("Laser Configuration")
+ laser_params_layout = QVBoxLayout()
+
+ # Create Laser Parameters Button
+ self.laserParametersButton = QPushButton("Laser Parameters")
+ self.laserParametersButton.clicked.connect(self.open_laser_parameters)
+
+ # Add button to layout
+ laser_params_layout.addWidget(self.laserParametersButton)
+
+ # Set layout for group box
+ laser_params_group.setLayout(laser_params_layout)
+
+ # Add to right panel or main layout
+ if right_panel and hasattr(right_panel, 'layout'):
+ right_panel.layout().addWidget(laser_params_group)
+ else:
+ # Fallback to main layout
+ if hasattr(self, 'layout'):
+ self.layout().addWidget(laser_params_group)
+
+ def open_laser_parameters(self):
+ """Open the Laser Parameters Dialog."""
+ try:
+ # Get layer count from the layer queue manager if available
+ layer_count = 0
+
+ if hasattr(self.main_window, 'multi_layer_controller') and hasattr(self.main_window.multi_layer_controller, 'layer_manager'):
+ layer_count = self.main_window.multi_layer_controller.layer_manager.total_layers
+
+ # Create and open the dialog
+ dialog = LaserParametersDialog(self.main_window.scancard, self, layer_count=layer_count)
+ dialog.exec_()
+
+ except Exception as e:
+ print(f"Error opening laser parameters dialog: {e}")
+ # Use simpler initialization that should work in any mode
+ dialog = LaserParametersDialog(self.main_window.scancard, self)
+ dialog.exec_()
+
def load_ui(self):
try:
- uic.loadUi('src/ui/control_screen/control_screen.ui', self)
+ uic.loadUi('ui/control_screen/control_screen.ui', self)
print("ControlScreen UI loaded successfully")
except Exception as e:
print(f"Failed to load ControlScreen UI: {e}")
@@ -92,12 +146,9 @@ def initialize_ui_elements(self):
self.moveToStartingPositionButton = self.findChild(QPushButton, "moveToStartingPositionButton")
self.prepareForPartRemovalButton = self.findChild(QPushButton, "prepareForPartRemovalButton")
- self.maxTempLabel = self.findChild(QLabel, "maxTempLabel") # Find the maxTempLabel
-
- # Initialize scanCardStatusLabel
+ self.maxTempLabel = self.findChild(QLabel, "maxTempLabel")
self.scanCardStatusLabel = self.findChild(QLabel, "scanCardStatusLabel")
- # Initialize start and stop marking buttons
self.startMarkingButton = self.findChild(QPushButton, "startMarkingButton")
self.stopMarkingButton = self.findChild(QPushButton, "stopMarkingButton")
@@ -131,10 +182,50 @@ def setup_connections(self):
self.moveToStartingPositionButton.clicked.connect(lambda: self.run_async_process(self.process_automation_controller.move_to_starting_sequence))
self.prepareForPartRemovalButton.clicked.connect(lambda: self.run_async_process(self.process_automation_controller.prepare_for_part_removal_sequence))
- # Connect start and stop marking buttons to Scancard functions
self.startMarkingButton.clicked.connect(self.main_window.scancard.start_mark)
self.stopMarkingButton.clicked.connect(self.main_window.scancard.stop_mark)
+ def setup_multi_layer_controls(self):
+ """Set up controls for multi-layer printing."""
+ try:
+ # Create group box for multi-layer printing
+ multi_layer_group = QGroupBox("Multi-Layer Print")
+ multi_layer_layout = QVBoxLayout()
+
+ # Folder selection button
+ self.select_folder_btn = QPushButton("Select Layers Folder")
+ self.select_folder_btn.clicked.connect(self.select_layers_folder)
+ multi_layer_layout.addWidget(self.select_folder_btn)
+
+ # Layer queue widget
+ self.layer_queue_widget = LayerQueueWidget()
+ multi_layer_layout.addWidget(self.layer_queue_widget)
+
+ # Start/resume buttons
+ buttons_layout = QHBoxLayout()
+ self.start_multi_print_btn = QPushButton("Start Multi-Layer Print")
+ self.start_multi_print_btn.clicked.connect(self.start_multi_layer_print)
+ self.start_multi_print_btn.setEnabled(False)
+ buttons_layout.addWidget(self.start_multi_print_btn)
+
+ self.resume_print_btn = QPushButton("Resume Previous Print")
+ self.resume_print_btn.clicked.connect(self.resume_print)
+ buttons_layout.addWidget(self.resume_print_btn)
+
+ multi_layer_layout.addLayout(buttons_layout)
+
+ multi_layer_group.setLayout(multi_layer_layout)
+
+ # Find where to add in the existing layout
+ right_panel = self.findChild(QWidget, "rightPanel")
+ if right_panel and hasattr(right_panel, "layout"):
+ right_panel.layout().addWidget(multi_layer_group)
+ else:
+ # Fall back to adding to the main layout
+ self.layout().addWidget(multi_layer_group)
+ except Exception as e:
+ print(f"Error setting up multi-layer controls: {e}")
+
@run_async
def run_async_send_gcode(self, gcode):
self.main_window.moonraker_api.send_gcode(gcode)
@@ -159,13 +250,50 @@ def setup_custom_widgets(self):
def connect_signals(self):
self.main_window.printer_status.temperatures_updated.connect(self.update_thermal_camera_widget)
self.main_window.printer_status.rgb_frame_updated.connect(self.update_rgb_camera_widget)
- self.main_window.printer_status.maxtemp_updated.connect(self.update_max_temp_label) # Connect the maxtemp_updated signal
+ self.main_window.printer_status.maxtemp_updated.connect(self.update_max_temp_label)
@pyqtSlot(float)
def update_max_temp_label(self, max_temp):
"""Slot to update the text of maxTempLabel with the maximum temperature."""
self.maxTempLabel.setText(f"Max Temp: {max_temp:.2f}°C")
+
+
+
+ def select_layers_folder(self):
+ """Open a file dialog to select a folder containing layer files."""
+ folder_path = QFileDialog.getExistingDirectory(self, "Select Layers Folder")
+ if folder_path:
+ layer_files = self.main_window.multi_layer_controller.load_layer_files(folder_path)
+ self.layer_queue_widget.set_layer_files(layer_files)
+ self.start_multi_print_btn.setEnabled(len(layer_files) > 0)
+
+ def start_multi_layer_print(self):
+ """Start the multi-layer print process."""
+ self.main_window.multi_layer_controller.start_multi_layer_print()
+
+ def resume_print(self):
+ """Open a file dialog to select a print state file and resume printing."""
+ state_file, _ = QFileDialog.getOpenFileName(self, "Select Print State File", "", "Print State Files (*.pstate)")
+ if state_file:
+ self.main_window.resume_print_from_saved_state(state_file)
+
+ @pyqtSlot(np.ndarray, dict)
+ def update_thermal_camera_widget(self, frame, temps):
+ """Update the thermal camera widget."""
+ if frame is not None:
+ image = QImage(frame.data, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_BGR888)
+ self.thermalCameraWidget.setImage(image)
+
+ @pyqtSlot(np.ndarray)
+ def update_rgb_camera_widget(self, frame):
+ """Update the RGB camera widget."""
+ if frame is not None:
+ height, width, channel = frame.shape
+ bytes_per_line = 3 * width
+ image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
+ self.rgbCameraWidget.setImage(image)
+
def set_motion_control_buttons_enabled(self, enabled):
"""Enable or disable motion control buttons."""
for button in self.motion_control_buttons:
@@ -184,7 +312,7 @@ def confirm_initial_levelling_recoat(self):
def confirm_heated_buffer_recoat(self):
"""Show a confirmation dialog before starting the heated buffer recoat."""
reply = QMessageBox.question(self, 'Confirmation',
- 'Ensure that the build module is moved to the starting position and recoater is homed before starting the heated buffer recoat. Do you want to proceed?',
+ 'Ensure that the build module is moved to the starting position and recoater is homed before starting the heated buffer recoat. Do you want to proceed?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
self.process_automation_controller.process_running = True
@@ -203,22 +331,6 @@ def update_setpoint(self, value):
self.main_window.printer_status.chamberTemperatureSetpoint = value
print(f"Chamber temperature setpoint updated to {value}")
- @pyqtSlot(np.ndarray, dict)
- def update_thermal_camera_widget(self, frame, temps):
- """Update the thermal camera widget."""
- if frame is not None:
- image = QImage(frame.data, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_BGR888)
- self.thermalCameraWidget.setImage(image)
-
- @pyqtSlot(np.ndarray)
- def update_rgb_camera_widget(self, frame):
- """Update the RGB camera widget."""
- if frame is not None:
- height, width, channel = frame.shape
- bytes_per_line = 3 * width
- image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
- self.rgbCameraWidget.setImage(image)
-
def cooldown(self):
"""Cooldown the chamber."""
self.main_window.printer_status.chamberTemperatureSetpoint = 0
diff --git a/src/ui/home_screen/home_screen.py b/src/ui/home_screen/home_screen.py
index da967109..57a9fd2b 100644
--- a/src/ui/home_screen/home_screen.py
+++ b/src/ui/home_screen/home_screen.py
@@ -16,7 +16,7 @@ def __init__(self, main_window):
# Load the UI file
try:
- uic.loadUi('src/ui/home_screen/home_screen.ui', self)
+ uic.loadUi('ui/home_screen/home_screen.ui', self)
print("HomeScreen UI loaded successfully")
except Exception as e:
print(f"Failed to load UI file: {e}")
diff --git a/src/ui/laser_parameters/__init__.py b/src/ui/laser_parameters/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/ui/laser_parameters/laser_parameters_dialog.py b/src/ui/laser_parameters/laser_parameters_dialog.py
new file mode 100644
index 00000000..e7f4d7bd
--- /dev/null
+++ b/src/ui/laser_parameters/laser_parameters_dialog.py
@@ -0,0 +1,681 @@
+from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QTabWidget, QWidget,
+ QFormLayout, QLabel, QDoubleSpinBox, QSpinBox,
+ QComboBox, QCheckBox, QDialogButtonBox, QGroupBox,
+ QScrollArea, QMessageBox, QProgressDialog, QHBoxLayout)
+from PyQt5.QtCore import Qt
+import yaml
+import os
+
+class LaserParametersDialog(QDialog):
+ def __init__(self, scancard, parent=None, layer_count=0):
+ super().__init__(parent)
+
+ # Dialog setup
+ self.setWindowTitle("Laser Marking Parameters")
+ self.setGeometry(100, 100, 700, 900)
+
+ # Store the layer count from the layer queue manager
+ self.layer_count = layer_count
+
+ # Main layout
+ main_layout = QVBoxLayout()
+ self.setLayout(main_layout)
+
+ # Tab Widget
+ self.tab_widget = QTabWidget()
+ main_layout.addWidget(self.tab_widget)
+
+ # Marking Parameters Tab
+ marking_tab = QWidget()
+ marking_layout = QVBoxLayout(marking_tab)
+
+ # Create a scroll area for marking parameters
+ scroll_area = QScrollArea()
+ scroll_area.setWidgetResizable(True)
+ marking_layout.addWidget(scroll_area)
+
+ # Container widget for the scroll area
+ marking_container = QWidget()
+ scroll_area.setWidget(marking_container)
+ marking_form = QFormLayout(marking_container)
+
+ # Motion Parameters Group
+ motion_group = QGroupBox("Motion Parameters")
+ motion_form = QFormLayout(motion_group)
+
+ # Marking Speed
+ self.mark_speed_spinbox = QDoubleSpinBox()
+ self.mark_speed_spinbox.setRange(0, 10000)
+ self.mark_speed_spinbox.setSingleStep(100)
+ self.mark_speed_spinbox.setValue(3000)
+ self.mark_speed_spinbox.setSuffix(" mm/s")
+ motion_form.addRow("Marking Speed:", self.mark_speed_spinbox)
+
+ # Jump Speed
+ self.jump_speed_spinbox = QDoubleSpinBox()
+ self.jump_speed_spinbox.setRange(0, 10000)
+ self.jump_speed_spinbox.setSingleStep(100)
+ self.jump_speed_spinbox.setValue(5000)
+ self.jump_speed_spinbox.setSuffix(" mm/s")
+ motion_form.addRow("Jump Speed:", self.jump_speed_spinbox)
+
+ marking_form.addRow(motion_group)
+
+ # Timing Parameters Group
+ timing_group = QGroupBox("Timing Parameters")
+ timing_form = QFormLayout(timing_group)
+
+ # Jump Delay
+ self.jump_delay_spinbox = QSpinBox()
+ self.jump_delay_spinbox.setRange(0, 10000)
+ self.jump_delay_spinbox.setSingleStep(10)
+ self.jump_delay_spinbox.setValue(100)
+ self.jump_delay_spinbox.setSuffix(" μs")
+ timing_form.addRow("Jump Delay:", self.jump_delay_spinbox)
+
+ # Laser On Delay
+ self.laser_on_delay_spinbox = QSpinBox()
+ self.laser_on_delay_spinbox.setRange(0, 10000)
+ self.laser_on_delay_spinbox.setSingleStep(10)
+ self.laser_on_delay_spinbox.setValue(100)
+ self.laser_on_delay_spinbox.setSuffix(" μs")
+ timing_form.addRow("Laser On Delay:", self.laser_on_delay_spinbox)
+
+ # Polygon Delay
+ self.polygon_delay_spinbox = QSpinBox()
+ self.polygon_delay_spinbox.setRange(0, 10000)
+ self.polygon_delay_spinbox.setSingleStep(10)
+ self.polygon_delay_spinbox.setValue(100)
+ self.polygon_delay_spinbox.setSuffix(" μs")
+ timing_form.addRow("Polygon Delay:", self.polygon_delay_spinbox)
+
+ # Laser Off Delay
+ self.laser_off_delay_spinbox = QSpinBox()
+ self.laser_off_delay_spinbox.setRange(0, 10000)
+ self.laser_off_delay_spinbox.setSingleStep(10)
+ self.laser_off_delay_spinbox.setValue(100)
+ self.laser_off_delay_spinbox.setSuffix(" μs")
+ timing_form.addRow("Laser Off Delay:", self.laser_off_delay_spinbox)
+
+ # Polygon Killer Time
+ self.polygon_killer_time_spinbox = QSpinBox()
+ self.polygon_killer_time_spinbox.setRange(0, 10000)
+ self.polygon_killer_time_spinbox.setSingleStep(10)
+ self.polygon_killer_time_spinbox.setValue(100)
+ self.polygon_killer_time_spinbox.setSuffix(" μs")
+ timing_form.addRow("Polygon Killer Time:", self.polygon_killer_time_spinbox)
+
+ marking_form.addRow(timing_group)
+
+ # Laser Parameters Group
+ laser_group = QGroupBox("Laser Parameters")
+ laser_form = QFormLayout(laser_group)
+
+ # Laser Frequency
+ self.laser_frequency_spinbox = QSpinBox()
+ self.laser_frequency_spinbox.setRange(0, 1000)
+ self.laser_frequency_spinbox.setSingleStep(1)
+ self.laser_frequency_spinbox.setValue(100)
+ self.laser_frequency_spinbox.setSuffix(" kHz")
+ laser_form.addRow("Laser Frequency:", self.laser_frequency_spinbox)
+
+ # Current (YAG/SPI)
+ self.current_spinbox = QSpinBox()
+ self.current_spinbox.setRange(0, 1000)
+ self.current_spinbox.setSingleStep(1)
+ self.current_spinbox.setValue(100)
+ self.current_spinbox.setSuffix(" A")
+ laser_form.addRow("Current:", self.current_spinbox)
+
+ # First Pulse Killer Length
+ self.first_pulse_killer_length_spinbox = QSpinBox()
+ self.first_pulse_killer_length_spinbox.setRange(0, 1000)
+ self.first_pulse_killer_length_spinbox.setSingleStep(1)
+ self.first_pulse_killer_length_spinbox.setValue(100)
+ self.first_pulse_killer_length_spinbox.setSuffix(" μs")
+ laser_form.addRow("First Pulse Killer Length:", self.first_pulse_killer_length_spinbox)
+
+ # Pulse Width
+ self.pulse_width_spinbox = QSpinBox()
+ self.pulse_width_spinbox.setRange(0, 1000)
+ self.pulse_width_spinbox.setSingleStep(1)
+ self.pulse_width_spinbox.setValue(100)
+ self.pulse_width_spinbox.setSuffix(" μs")
+ laser_form.addRow("Pulse Width:", self.pulse_width_spinbox)
+
+ # First Pulse Width
+ self.first_pulse_width_spinbox = QSpinBox()
+ self.first_pulse_width_spinbox.setRange(0, 100)
+ self.first_pulse_width_spinbox.setSingleStep(1)
+ self.first_pulse_width_spinbox.setValue(100)
+ self.first_pulse_width_spinbox.setSuffix(" %")
+ laser_form.addRow("First Pulse Width:", self.first_pulse_width_spinbox)
+
+ # Increment Step
+ self.increment_step_spinbox = QSpinBox()
+ self.increment_step_spinbox.setRange(0, 100)
+ self.increment_step_spinbox.setSingleStep(1)
+ self.increment_step_spinbox.setValue(100)
+ self.increment_step_spinbox.setSuffix(" %")
+ laser_form.addRow("Increment Step:", self.increment_step_spinbox)
+
+ marking_form.addRow(laser_group)
+
+ self.tab_widget.addTab(marking_tab, "Marking Parameters")
+
+ # Fill Parameters Tab
+ fill_tab = QWidget()
+ fill_layout = QVBoxLayout(fill_tab)
+
+ # Create a scroll area for fill parameters
+ fill_scroll_area = QScrollArea()
+ fill_scroll_area.setWidgetResizable(True)
+ fill_layout.addWidget(fill_scroll_area)
+
+ # Container widget for the scroll area
+ fill_container = QWidget()
+ fill_scroll_area.setWidget(fill_container)
+ fill_form = QFormLayout(fill_container)
+
+ # Fill Parameters Group
+ fill_group = QGroupBox("Entity Fill Properties")
+ fill_param_form = QFormLayout(fill_group)
+
+ # Fill Mode Combo Box
+ self.fill_mode_combobox = QComboBox()
+ self.fill_mode_combobox.addItems([
+ "No Filling",
+ "One-way Filling",
+ "Two-way Filling",
+ "Bow-shaped Filling",
+ "Back-shaped Filling"
+ ])
+ fill_param_form.addRow("Fill Mode:", self.fill_mode_combobox)
+
+ # Fill Parameters Checkboxes
+ self.equal_distance_checkbox = QCheckBox()
+ fill_param_form.addRow("Evenly Distribute Fill Lines:", self.equal_distance_checkbox)
+
+ self.second_fill_checkbox = QCheckBox()
+ fill_param_form.addRow("Enable Second Padding:", self.second_fill_checkbox)
+
+ self.rotate_angle_checkbox = QCheckBox()
+ fill_param_form.addRow("Automatic Rotation Angle:", self.rotate_angle_checkbox)
+
+ self.fill_as_one_checkbox = QCheckBox()
+ fill_param_form.addRow("Calculate Objects as a Whole:", self.fill_as_one_checkbox)
+
+ self.more_intact_checkbox = QCheckBox()
+ fill_param_form.addRow("Optimize Two-way Filling:", self.more_intact_checkbox)
+
+ self.fill_3d_checkbox = QCheckBox()
+ fill_param_form.addRow("Use Triangle Fill Mode:", self.fill_3d_checkbox)
+
+ # Fill Parameters SpinBoxes
+ self.loop_num_spinbox = QSpinBox()
+ self.loop_num_spinbox.setRange(0, 100)
+ self.loop_num_spinbox.setValue(1)
+ fill_param_form.addRow("Number of Boundary Rings:", self.loop_num_spinbox)
+
+ self.fill_mark_times_spinbox = QSpinBox()
+ self.fill_mark_times_spinbox.setRange(1, 100)
+ self.fill_mark_times_spinbox.setValue(1)
+ fill_param_form.addRow("Number of Markings for Current Angle:", self.fill_mark_times_spinbox)
+
+ self.cur_mark_times_spinbox = QSpinBox()
+ self.cur_mark_times_spinbox.setRange(1, 100)
+ self.cur_mark_times_spinbox.setValue(12)
+ fill_param_form.addRow("Current Number of Markings:", self.cur_mark_times_spinbox)
+
+ self.layer_id_spinbox = QSpinBox()
+ self.layer_id_spinbox.setRange(0, 255)
+ self.layer_id_spinbox.setValue(1)
+ fill_param_form.addRow("Fill in Pen Number:", self.layer_id_spinbox)
+
+ self.fill_space_spinbox = QDoubleSpinBox()
+ self.fill_space_spinbox.setRange(0.01, 1000)
+ self.fill_space_spinbox.setValue(100)
+ fill_param_form.addRow("Fill Line Space:", self.fill_space_spinbox)
+
+ self.fill_angle_spinbox = QDoubleSpinBox()
+ self.fill_angle_spinbox.setRange(0, 360)
+ self.fill_angle_spinbox.setValue(100)
+ fill_param_form.addRow("Fill Angle:", self.fill_angle_spinbox)
+
+ self.fill_edge_offset_spinbox = QDoubleSpinBox()
+ self.fill_edge_offset_spinbox.setRange(0, 1000)
+ self.fill_edge_offset_spinbox.setValue(100)
+ fill_param_form.addRow("Fill Edge Offset:", self.fill_edge_offset_spinbox)
+
+ self.fill_start_offset_spinbox = QDoubleSpinBox()
+ self.fill_start_offset_spinbox.setRange(0, 1000)
+ self.fill_start_offset_spinbox.setValue(100)
+ fill_param_form.addRow("Fill Start Offset:", self.fill_start_offset_spinbox)
+
+ self.fill_end_offset_spinbox = QDoubleSpinBox()
+ self.fill_end_offset_spinbox.setRange(0, 1000)
+ self.fill_end_offset_spinbox.setValue(100)
+ fill_param_form.addRow("Fill End Offset:", self.fill_end_offset_spinbox)
+
+ self.fill_line_reduction_spinbox = QDoubleSpinBox()
+ self.fill_line_reduction_spinbox.setRange(0, 1000)
+ self.fill_line_reduction_spinbox.setValue(100)
+ fill_param_form.addRow("Fill Line Reduction:", self.fill_line_reduction_spinbox)
+
+ self.loop_space_spinbox = QDoubleSpinBox()
+ self.loop_space_spinbox.setRange(0, 1000)
+ self.loop_space_spinbox.setValue(100)
+ fill_param_form.addRow("Loop Space:", self.loop_space_spinbox)
+
+ self.second_angle_spinbox = QDoubleSpinBox()
+ self.second_angle_spinbox.setRange(0, 360)
+ self.second_angle_spinbox.setValue(100)
+ fill_param_form.addRow("Second Fill Angle:", self.second_angle_spinbox)
+
+ self.rotate_angle_spinbox = QDoubleSpinBox()
+ self.rotate_angle_spinbox.setRange(0, 360)
+ self.rotate_angle_spinbox.setValue(100)
+ fill_param_form.addRow("Angle of Each Increment:", self.rotate_angle_spinbox)
+
+ fill_form.addRow(fill_group)
+
+ self.tab_widget.addTab(fill_tab, "Fill Parameters")
+
+ # Add "Apply to All Layers" option
+ apply_options_layout = QHBoxLayout()
+ self.apply_all_layers_checkbox = QCheckBox("Apply to All Layers")
+ self.apply_all_layers_checkbox.setChecked(True) # Default to checked
+ apply_options_layout.addWidget(self.apply_all_layers_checkbox)
+
+ # Add max layer input
+ self.max_layer_label = QLabel("Maximum Layer:")
+ self.max_layer_spinbox = QSpinBox()
+ self.max_layer_spinbox.setRange(1, 255)
+
+ # Use the provided layer count or default to 10
+ self.max_layer_spinbox.setValue(self.layer_count if self.layer_count > 0 else 10)
+
+ apply_options_layout.addWidget(self.max_layer_label)
+ apply_options_layout.addWidget(self.max_layer_spinbox)
+
+ # Add the options layout
+ main_layout.addLayout(apply_options_layout)
+
+ # Dialog Buttons
+ button_box = QDialogButtonBox(
+ QDialogButtonBox.Save | QDialogButtonBox.Cancel
+ )
+ button_box.accepted.connect(self.save_parameters)
+ button_box.rejected.connect(self.reject)
+ main_layout.addWidget(button_box)
+
+ # Initialize scancard and load parameters
+ self.scancard = scancard
+ self.load_parameters()
+
+ def load_parameters(self):
+ """Load parameters from scancard and update UI."""
+ try:
+ # Get the marking parameters for layer 1
+ future = self.scancard.get_markParameters_by_layer(1)
+ response = future.result()
+
+ if response and response.get("ret_value") == 1:
+ data = response.get("response", {}).get("data", {})
+
+ # Update UI with marking parameters
+ self.mark_speed_spinbox.setValue(data.get("markSpeed", 3000))
+ self.jump_speed_spinbox.setValue(data.get("jumpSpeed", 5000))
+ self.jump_delay_spinbox.setValue(data.get("jumpDelay", 100))
+ self.laser_on_delay_spinbox.setValue(data.get("laserOnDelay", 100))
+ self.polygon_delay_spinbox.setValue(data.get("polygonDelay", 100))
+ self.laser_off_delay_spinbox.setValue(data.get("laserOffDelay", 100))
+ self.polygon_killer_time_spinbox.setValue(data.get("polygonKillerTime", 100))
+ self.laser_frequency_spinbox.setValue(data.get("laserFrequency", 100))
+ self.current_spinbox.setValue(data.get("current", 100))
+ self.first_pulse_killer_length_spinbox.setValue(data.get("firstPulseKillerLength", 100))
+ self.pulse_width_spinbox.setValue(data.get("pulseWidth", 100))
+ self.first_pulse_width_spinbox.setValue(data.get("firstPulseWidth", 100))
+ self.increment_step_spinbox.setValue(data.get("incrementStep", 100))
+
+ # Get the fill parameters for index 1
+ future = self.scancard.get_entity_fill_property_by_index(1, 1)
+ response = future.result()
+
+ if response and response.get("ret_value") == 1:
+ data = response.get("response", {}).get("data", {})
+
+ # Update UI with fill parameters
+ self.fill_mode_combobox.setCurrentIndex(data.get("fill_mode", 0))
+ self.equal_distance_checkbox.setChecked(data.get("bEqualDistance", False))
+ self.second_fill_checkbox.setChecked(data.get("bSecondFill", False))
+ self.rotate_angle_checkbox.setChecked(data.get("bRotateAngle", False))
+ self.fill_as_one_checkbox.setChecked(data.get("bFillAsOne", False))
+ self.more_intact_checkbox.setChecked(data.get("bMoreIntact", False))
+ self.fill_3d_checkbox.setChecked(data.get("bFill3D", False))
+ self.loop_num_spinbox.setValue(data.get("loopNum", 1))
+ self.fill_mark_times_spinbox.setValue(data.get("iFillMarkTimes", 1))
+ self.cur_mark_times_spinbox.setValue(data.get("iCurMarkTimes", 12))
+ self.layer_id_spinbox.setValue(data.get("layerId", 1))
+ self.fill_space_spinbox.setValue(data.get("fillSpace", 100))
+ self.fill_angle_spinbox.setValue(data.get("fillAngle", 100))
+ self.fill_edge_offset_spinbox.setValue(data.get("fillEdgeOffset", 100))
+ self.fill_start_offset_spinbox.setValue(data.get("fillStartOffset", 100))
+ self.fill_end_offset_spinbox.setValue(data.get("fillEndOffset", 100))
+ self.fill_line_reduction_spinbox.setValue(data.get("fillLineReduction", 100))
+ self.loop_space_spinbox.setValue(data.get("loopSpace", 100))
+ self.second_angle_spinbox.setValue(data.get("secondAngle", 100))
+ self.rotate_angle_spinbox.setValue(data.get("dRotateAngle", 100))
+
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ "Parameter Load Error",
+ f"Failed to load parameters from Scancard: {str(e)}"
+ )
+ self.reset_to_defaults()
+
+ def reset_to_defaults(self):
+ """Set default parameter values."""
+ # Set default marking parameters
+ self.mark_speed_spinbox.setValue(3000)
+ self.jump_speed_spinbox.setValue(5000)
+ self.jump_delay_spinbox.setValue(100)
+ self.laser_on_delay_spinbox.setValue(100)
+ self.polygon_delay_spinbox.setValue(100)
+ self.laser_off_delay_spinbox.setValue(100)
+ self.polygon_killer_time_spinbox.setValue(100)
+ self.laser_frequency_spinbox.setValue(100)
+ self.current_spinbox.setValue(100)
+ self.first_pulse_killer_length_spinbox.setValue(100)
+ self.pulse_width_spinbox.setValue(100)
+ self.first_pulse_width_spinbox.setValue(100)
+ self.increment_step_spinbox.setValue(100)
+
+ # Set default fill parameters
+ self.fill_mode_combobox.setCurrentIndex(0)
+ self.equal_distance_checkbox.setChecked(False)
+ self.second_fill_checkbox.setChecked(False)
+ self.rotate_angle_checkbox.setChecked(False)
+ self.fill_as_one_checkbox.setChecked(False)
+ self.more_intact_checkbox.setChecked(False)
+ self.fill_3d_checkbox.setChecked(False)
+ self.loop_num_spinbox.setValue(1)
+ self.fill_mark_times_spinbox.setValue(1)
+ self.cur_mark_times_spinbox.setValue(12)
+ self.layer_id_spinbox.setValue(1)
+ self.fill_space_spinbox.setValue(100)
+ self.fill_angle_spinbox.setValue(100)
+ self.fill_edge_offset_spinbox.setValue(100)
+ self.fill_start_offset_spinbox.setValue(100)
+ self.fill_end_offset_spinbox.setValue(100)
+ self.fill_line_reduction_spinbox.setValue(100)
+ self.loop_space_spinbox.setValue(100)
+ self.second_angle_spinbox.setValue(100)
+ self.rotate_angle_spinbox.setValue(100)
+
+ def save_parameters(self):
+ """Save parameters to scancard with improved error handling and cancellation support."""
+ # Collect marking parameters
+ marking_params = {
+ "markSpeed": self.mark_speed_spinbox.value(),
+ "jumpSpeed": self.jump_speed_spinbox.value(),
+ "jumpDelay": self.jump_delay_spinbox.value(),
+ "laserOnDelay": self.laser_on_delay_spinbox.value(),
+ "polygonDelay": self.polygon_delay_spinbox.value(),
+ "laserOffDelay": self.laser_off_delay_spinbox.value(),
+ "polygonKillerTime": self.polygon_killer_time_spinbox.value(),
+ "laserFrequency": self.laser_frequency_spinbox.value(),
+ "current": self.current_spinbox.value(),
+ "firstPulseKillerLength": self.first_pulse_killer_length_spinbox.value(),
+ "pulseWidth": self.pulse_width_spinbox.value(),
+ "firstPulseWidth": self.first_pulse_width_spinbox.value(),
+ "incrementStep": self.increment_step_spinbox.value()
+ }
+
+ # Collect fill parameters
+ fill_params = {
+ "fill_mode": self.fill_mode_combobox.currentIndex(),
+ "bEqualDistance": self.equal_distance_checkbox.isChecked(),
+ "bSecondFill": self.second_fill_checkbox.isChecked(),
+ "bRotateAngle": self.rotate_angle_checkbox.isChecked(),
+ "bFillAsOne": self.fill_as_one_checkbox.isChecked(),
+ "bMoreIntact": self.more_intact_checkbox.isChecked(),
+ "bFill3D": self.fill_3d_checkbox.isChecked(),
+ "loopNum": self.loop_num_spinbox.value(),
+ "iFillMarkTimes": self.fill_mark_times_spinbox.value(),
+ "iCurMarkTimes": self.cur_mark_times_spinbox.value(),
+ "layerId": self.layer_id_spinbox.value(),
+ "fillSpace": self.fill_space_spinbox.value(),
+ "fillAngle": self.fill_angle_spinbox.value(),
+ "fillEdgeOffset": self.fill_edge_offset_spinbox.value(),
+ "fillStartOffset": self.fill_start_offset_spinbox.value(),
+ "fillEndOffset": self.fill_end_offset_spinbox.value(),
+ "fillLineReduction": self.fill_line_reduction_spinbox.value(),
+ "loopSpace": self.loop_space_spinbox.value(),
+ "secondAngle": self.second_angle_spinbox.value(),
+ "dRotateAngle": self.rotate_angle_spinbox.value()
+ }
+
+ try:
+ # Check if we should apply to all layers
+ apply_all = self.apply_all_layers_checkbox.isChecked()
+ max_layer = self.max_layer_spinbox.value() if apply_all else 1
+
+ if apply_all:
+ # Create progress dialog
+ progress = QProgressDialog("Validating layers...", "Cancel", 0, max_layer, self)
+ progress.setWindowModality(Qt.WindowModal)
+ progress.setMinimumDuration(0)
+ progress.setValue(0)
+ progress.show()
+
+ # First validate all layers and entities exist
+ valid_layers = []
+ invalid_layers = []
+
+ for layer in range(1, max_layer + 1):
+ if progress.wasCanceled():
+ if valid_layers:
+ # Ask user if they want to proceed with validated layers only
+ reply = QMessageBox.question(
+ self,
+ "Validation Canceled",
+ f"Continue with {len(valid_layers)} validated layers only?",
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+ if reply != QMessageBox.Yes:
+ progress.close()
+ return
+ else:
+ progress.close()
+ return
+
+ progress.setValue(layer)
+ progress.setLabelText(f"Validating layer {layer} of {max_layer}...")
+
+ # Validate layer and entity
+ future = self.scancard.validate_layer_entity(layer)
+ result = future.result()
+
+ if result.get("valid", False):
+ valid_layers.append(layer)
+ else:
+ invalid_layers.append((layer, result.get("message", "Unknown error")))
+
+ # Report validation results
+ if invalid_layers:
+ message = "The following layers could not be validated:\n\n"
+ message += "\n".join([f"Layer {layer}: {msg}" for layer, msg in invalid_layers])
+ message += "\n\nDo you want to continue with valid layers only?"
+
+ reply = QMessageBox.question(
+ self,
+ "Validation Results",
+ message,
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+ if reply != QMessageBox.Yes:
+ progress.close()
+ return
+
+ # Proceed with valid layers only
+ if not valid_layers:
+ QMessageBox.critical(
+ self,
+ "Validation Error",
+ "No valid layers found to apply parameters to."
+ )
+ progress.close()
+ return
+
+ # Reset progress for applying parameters
+ progress.setLabelText("Applying parameters...")
+ progress.setRange(0, len(valid_layers))
+ progress.setValue(0)
+
+ # Track failures
+ failures = []
+ successful_layers = []
+
+ # Apply to valid layers
+ for i, layer in enumerate(valid_layers):
+ if progress.wasCanceled():
+ if successful_layers:
+ reply = QMessageBox.question(
+ self,
+ "Operation Canceled",
+ f"Parameters were applied to {len(successful_layers)} layers.\n\n"
+ "Do you want to download these changes to the device?",
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.Yes
+ )
+ if reply == QMessageBox.Yes:
+ break # Continue to download step
+ else:
+ progress.close()
+ return # Cancel without downloading
+ else:
+ progress.close()
+ return
+
+ progress.setValue(i)
+ progress.setLabelText(f"Applying parameters to layer {layer}...")
+
+ # Update marking parameters for the current layer
+ future = self.scancard.set_markParameters_by_layer(layer, marking_params)
+ response = future.result()
+
+ if not response or response.get("ret_value") != 1:
+ failures.append(f"Layer {layer} marking parameters")
+ continue
+
+ # Update fill parameters for the current entity in the current layer
+ future = self.scancard.set_entity_fill_property_by_index(layer, 1, fill_params)
+ response = future.result()
+
+ if not response or response.get("ret_value") != 1:
+ failures.append(f"Layer {layer} fill parameters")
+ continue
+
+ successful_layers.append(layer)
+
+ # Download parameters to apply changes
+ progress.setLabelText("Downloading parameters to device...")
+ progress.setValue(len(valid_layers))
+
+ future = self.scancard.download_parameters()
+ response = future.result()
+
+ if not response or response.get("ret_value") != 1:
+ failures.append("Downloading parameters")
+
+ progress.setValue(len(valid_layers))
+ progress.close()
+
+ # Report results
+ if failures:
+ QMessageBox.warning(
+ self,
+ "Parameter Update Warning",
+ f"Parameters were applied to {len(successful_layers)} out of {len(valid_layers)} layers, but with some failures:\n" +
+ "\n".join(failures)
+ )
+ else:
+ QMessageBox.information(
+ self,
+ "Success",
+ f"Parameters applied to {len(successful_layers)} layers successfully"
+ )
+ else:
+ # Apply only to layer 1 (original behavior)
+ # Validate layer 1 first
+ future = self.scancard.validate_layer_entity(1)
+ result = future.result()
+
+ if not result.get("valid", False):
+ QMessageBox.critical(
+ self,
+ "Validation Error",
+ f"Cannot apply parameters to layer 1: {result.get('message', 'Unknown error')}"
+ )
+ return
+
+ future = self.scancard.set_markParameters_by_layer(1, marking_params)
+ response = future.result()
+
+ if not response or response.get("ret_value") != 1:
+ raise Exception("Failed to set marking parameters")
+
+ future = self.scancard.set_entity_fill_property_by_index(1, 1, fill_params)
+ response = future.result()
+
+ if not response or response.get("ret_value") != 1:
+ raise Exception("Failed to set fill parameters")
+
+ future = self.scancard.download_parameters()
+ response = future.result()
+
+ if not response or response.get("ret_value") != 1:
+ raise Exception("Failed to download parameters")
+
+ QMessageBox.information(
+ self,
+ "Success",
+ "Parameters saved to layer 1 successfully"
+ )
+
+ # Save to YAML for future reference
+ self.save_to_yaml(marking_params, fill_params)
+
+ self.accept()
+
+ except Exception as e:
+ QMessageBox.critical(
+ self,
+ "Parameter Update Error",
+ f"Failed to update parameters: {str(e)}"
+ )
+
+ def save_to_yaml(self, marking_params, fill_params):
+ """Save parameters to YAML file for future reference."""
+ try:
+ # Combine all parameters
+ all_params = {
+ "marking_parameters": marking_params,
+ "fill_parameters": fill_params,
+ "apply_to_all_layers": self.apply_all_layers_checkbox.isChecked(),
+ "max_layer": self.max_layer_spinbox.value()
+ }
+
+ # Save to YAML
+ with open('laser_parameters.yaml', 'w') as file:
+ yaml.dump(all_params, file)
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ "YAML Save Error",
+ f"Failed to save parameters to YAML: {str(e)}"
+ )
\ No newline at end of file
diff --git a/src/ui/laser_parameters/laser_parameters_dialog.ui b/src/ui/laser_parameters/laser_parameters_dialog.ui
new file mode 100644
index 00000000..a82fc359
--- /dev/null
+++ b/src/ui/laser_parameters/laser_parameters_dialog.ui
@@ -0,0 +1,816 @@
+
+
+ LaserMarkingParametersDialog
+
+
+
+ 0
+ 0
+ 700
+ 900
+
+
+
+ Laser Marking Parameters
+
+
+ -
+
+
+ 0
+
+
+
+ Marking Parameters
+
+
+
-
+
+
+ true
+
+
+
+
-
+
+
+ Motion Parameters
+
+
+
-
+
+
+ Marking Speed:
+
+
+
+ -
+
+
+ mm/s
+
+
+ 0.000000000000000
+
+
+ 10000.000000000000000
+
+
+ 100.000000000000000
+
+
+ 3000.000000000000000
+
+
+
+ -
+
+
+ Jump Speed:
+
+
+
+ -
+
+
+ mm/s
+
+
+ 0.000000000000000
+
+
+ 10000.000000000000000
+
+
+ 100.000000000000000
+
+
+ 5000.000000000000000
+
+
+
+
+
+
+ -
+
+
+ Timing Parameters
+
+
+
-
+
+
+ Jump Delay:
+
+
+
+ -
+
+
+ μs
+
+
+ 0
+
+
+ 10000
+
+
+ 10
+
+
+ 100
+
+
+
+ -
+
+
+ Laser On Delay:
+
+
+
+ -
+
+
+ μs
+
+
+ 0
+
+
+ 10000
+
+
+ 10
+
+
+ 100
+
+
+
+ -
+
+
+ Polygon Delay:
+
+
+
+ -
+
+
+ μs
+
+
+ 0
+
+
+ 10000
+
+
+ 10
+
+
+ 100
+
+
+
+ -
+
+
+ Laser Off Delay:
+
+
+
+ -
+
+
+ μs
+
+
+ 0
+
+
+ 10000
+
+
+ 10
+
+
+ 100
+
+
+
+ -
+
+
+ Polygon Killer Time:
+
+
+
+ -
+
+
+ μs
+
+
+ 0
+
+
+ 10000
+
+
+ 10
+
+
+ 100
+
+
+
+
+
+
+ -
+
+
+ Laser Parameters
+
+
+
-
+
+
+ Laser Frequency:
+
+
+
+ -
+
+
+ kHz
+
+
+ 0
+
+
+ 1000
+
+
+ 100
+
+
+
+ -
+
+
+ Current:
+
+
+
+ -
+
+
+ A
+
+
+ 0
+
+
+ 1000
+
+
+ 100
+
+
+
+ -
+
+
+ First Pulse Killer Length:
+
+
+
+ -
+
+
+ μs
+
+
+ 0
+
+
+ 1000
+
+
+ 100
+
+
+
+ -
+
+
+ Pulse Width:
+
+
+
+ -
+
+
+ μs
+
+
+ 0
+
+
+ 1000
+
+
+ 100
+
+
+
+ -
+
+
+ First Pulse Width:
+
+
+
+ -
+
+
+ %
+
+
+ 0
+
+
+ 100
+
+
+ 100
+
+
+
+ -
+
+
+ Increment Step:
+
+
+
+ -
+
+
+ %
+
+
+ 0
+
+
+ 100
+
+
+ 100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fill Parameters
+
+
+ -
+
+
+ true
+
+
+
+
-
+
+
+ Entity Fill Properties
+
+
+
-
+
+
+ Fill Mode:
+
+
+
+ -
+
+
-
+
+ No Filling
+
+
+ -
+
+ One-way Filling
+
+
+ -
+
+ Two-way Filling
+
+
+ -
+
+ Bow-shaped Filling
+
+
+ -
+
+ Back-shaped Filling
+
+
+
+
+ -
+
+
+ Evenly Distribute Fill Lines:
+
+
+
+ -
+
+
+ -
+
+
+ Enable Second Padding:
+
+
+
+ -
+
+
+ -
+
+
+ Automatic Rotation Angle:
+
+
+
+ -
+
+
+ -
+
+
+ Calculate Objects as a Whole:
+
+
+
+ -
+
+
+ -
+
+
+ Optimize Two-way Filling:
+
+
+
+ -
+
+
+ -
+
+
+ Use Triangle Fill Mode:
+
+
+
+ -
+
+
+ -
+
+
+ Number of Boundary Rings:
+
+
+
+ -
+
+
+ 0
+
+
+ 100
+
+
+ 1
+
+
+
+ -
+
+
+ Number of Markings for Current Angle:
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+
+ -
+
+
+ Current Number of Markings:
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+ 12
+
+
+
+ -
+
+
+ Fill in Pen Number:
+
+
+
+ -
+
+
+ 0
+
+
+ 255
+
+
+ 1
+
+
+
+ -
+
+
+ Fill Line Space:
+
+
+
+ -
+
+
+ 0.010000000000000
+
+
+ 1000.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Fill Angle:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 360.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Fill Edge Offset:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 1000.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Fill Start Offset:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 1000.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Fill End Offset:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 1000.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Fill Line Reduction:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 1000.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Loop Space:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 1000.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Second Fill Angle:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 360.000000000000000
+
+
+ 100.000000000000000
+
+
+
+ -
+
+
+ Angle of Each Increment:
+
+
+
+ -
+
+
+ 0.000000000000000
+
+
+ 360.000000000000000
+
+
+ 100.000000000000000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Save
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ LaserMarkingParametersDialog
+ accept()
+
+
+ 349
+ 880
+
+
+ 349
+ 449
+
+
+
+
+ buttonBox
+ rejected()
+ LaserMarkingParametersDialog
+ reject()
+
+
+ 349
+ 880
+
+
+ 349
+ 449
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ui/layer_management/layer_queue_widget.py b/src/ui/layer_management/layer_queue_widget.py
new file mode 100644
index 00000000..fda9db67
--- /dev/null
+++ b/src/ui/layer_management/layer_queue_widget.py
@@ -0,0 +1,224 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QListWidget,
+ QListWidgetItem, QLabel, QProgressBar, QPushButton,
+ QMenu, QAction, QMessageBox)
+from PyQt5.QtCore import pyqtSignal, Qt, QSize
+from PyQt5.QtGui import QIcon, QColor, QBrush
+import os
+import logging
+
+class LayerQueueWidget(QWidget):
+ """
+ Widget for displaying and managing a queue of layer files.
+ """
+
+ layer_selected = pyqtSignal(int, str) # Index, filepath
+
+ def __init__(self):
+ """Initialize the layer queue widget."""
+ super().__init__()
+ self.layer_files = []
+ self.current_layer_index = -1
+
+ # Configure logging
+ self.logger = logging.getLogger(__name__)
+ formatter = logging.Formatter(
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+ )
+ handler = logging.StreamHandler()
+ handler.setFormatter(formatter)
+ self.logger.addHandler(handler)
+ self.logger.setLevel(logging.INFO)
+
+ self.initUI()
+
+ def initUI(self):
+ """Initialize the user interface components."""
+ layout = QVBoxLayout()
+
+ # Layer count and controls
+ header_layout = QHBoxLayout()
+
+ self.count_label = QLabel("Layers: 0")
+ header_layout.addWidget(self.count_label)
+
+ header_layout.addStretch()
+
+ self.refresh_button = QPushButton("Refresh")
+ self.refresh_button.setToolTip("Refresh the layer list")
+ self.refresh_button.clicked.connect(self.refresh_layer_list)
+ header_layout.addWidget(self.refresh_button)
+
+ layout.addLayout(header_layout)
+
+ # Layer list
+ self.layer_list = QListWidget()
+ self.layer_list.setSelectionMode(QListWidget.SingleSelection)
+ self.layer_list.itemClicked.connect(self.on_layer_selected)
+ self.layer_list.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.layer_list.customContextMenuRequested.connect(self.show_context_menu)
+
+ # Set size for items
+ self.layer_list.setIconSize(QSize(16, 16))
+
+ layout.addWidget(self.layer_list)
+
+ # Current progress section
+ progress_layout = QVBoxLayout()
+
+ progress_layout.addWidget(QLabel("Current Layer:"))
+ self.current_layer_label = QLabel("None")
+ progress_layout.addWidget(self.current_layer_label)
+
+ self.layer_progress = QProgressBar()
+ self.layer_progress.setRange(0, 100)
+ self.layer_progress.setValue(0)
+ progress_layout.addWidget(self.layer_progress)
+
+ layout.addLayout(progress_layout)
+
+ self.setLayout(layout)
+
+ def set_layer_files(self, layer_files):
+ """
+ Set the layer files to display.
+
+ Args:
+ layer_files (list): List of file paths to layer files
+ """
+ self.layer_files = layer_files
+ self.refresh_layer_list()
+
+ def refresh_layer_list(self):
+ """Refresh the layer list display."""
+ self.layer_list.clear()
+
+ for i, filepath in enumerate(self.layer_files):
+ filename = os.path.basename(filepath)
+ item = QListWidgetItem(f"{i+1}: {filename}")
+
+ # Set icon or styling based on layer status
+ if i < self.current_layer_index:
+ # Completed layer
+ item.setForeground(QBrush(QColor(100, 100, 100))) # Gray out completed
+ elif i == self.current_layer_index:
+ # Current layer
+ item.setForeground(QBrush(QColor(0, 100, 0))) # Green for current
+ font = item.font()
+ font.setBold(True)
+ item.setFont(font)
+
+ self.layer_list.addItem(item)
+
+ self.count_label.setText(f"Layers: {len(self.layer_files)}")
+
+ # Update current layer label and progress
+ self.update_current_layer(self.current_layer_index)
+
+ def on_layer_selected(self, item):
+ """
+ Handle layer selection.
+
+ Args:
+ item (QListWidgetItem): The selected item
+ """
+ index = self.layer_list.row(item)
+ if 0 <= index < len(self.layer_files):
+ self.layer_selected.emit(index, self.layer_files[index])
+ self.logger.debug(f"Layer {index} selected: {self.layer_files[index]}")
+
+ def update_current_layer(self, index):
+ """
+ Update the UI to show the current layer.
+
+ Args:
+ index (int): Index of the current layer
+ """
+ self.current_layer_index = index
+
+ # Update layer list selection
+ if 0 <= index < self.layer_list.count():
+ self.layer_list.setCurrentRow(index)
+
+ # Update layer info
+ filename = os.path.basename(self.layer_files[index])
+ self.current_layer_label.setText(f"{index + 1}/{len(self.layer_files)}: {filename}")
+
+ # Update progress
+ progress = int((index + 1) / len(self.layer_files) * 100)
+ self.layer_progress.setValue(progress)
+ else:
+ self.current_layer_label.setText("None")
+ self.layer_progress.setValue(0)
+
+ # Refresh the list to update styling
+ self.refresh_layer_list()
+
+ def show_context_menu(self, position):
+ """
+ Show a context menu for the layer list.
+
+ Args:
+ position: The position where to show the menu
+ """
+ if not self.layer_files:
+ return
+
+ menu = QMenu()
+
+ # Get the item at position
+ item = self.layer_list.itemAt(position)
+ if item:
+ index = self.layer_list.row(item)
+ view_action = menu.addAction("View Layer Details")
+ view_action.triggered.connect(lambda: self.show_layer_details(index))
+
+ if index != self.current_layer_index:
+ set_current_action = menu.addAction("Set as Current Layer")
+ set_current_action.triggered.connect(lambda: self.set_as_current_layer(index))
+
+ menu.exec_(self.layer_list.mapToGlobal(position))
+
+ def show_layer_details(self, index):
+ """
+ Show details for a specific layer.
+
+ Args:
+ index (int): Index of the layer to show
+ """
+ if 0 <= index < len(self.layer_files):
+ filepath = self.layer_files[index]
+ filename = os.path.basename(filepath)
+
+ details = f"Layer: {index + 1}/{len(self.layer_files)}\n"
+ details += f"Filename: {filename}\n"
+ details += f"Full path: {filepath}\n"
+ details += f"Status: {'Completed' if index < self.current_layer_index else 'Current' if index == self.current_layer_index else 'Pending'}"
+
+ QMessageBox.information(self, "Layer Details", details)
+
+ def set_as_current_layer(self, index):
+ """
+ Set a layer as the current layer.
+
+ Args:
+ index (int): Index of the layer to set as current
+ """
+ if 0 <= index < len(self.layer_files):
+ if index < self.current_layer_index:
+ # Moving backward - confirm
+ response = QMessageBox.question(
+ self,
+ "Confirm Layer Change",
+ f"Are you sure you want to mark layer {index + 1} as the current layer?\n\n"
+ f"This will mean layers {index + 1} to {self.current_layer_index + 1} will need to be processed again.",
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+
+ if response != QMessageBox.Yes:
+ return
+
+ self.update_current_layer(index)
+ self.layer_selected.emit(index, self.layer_files[index])
+ self.logger.info(f"Set layer {index} as current layer")
\ No newline at end of file
diff --git a/src/ui/loading_screen/loading_screen.py b/src/ui/loading_screen/loading_screen.py
index 0b5193e4..309c3bc4 100644
--- a/src/ui/loading_screen/loading_screen.py
+++ b/src/ui/loading_screen/loading_screen.py
@@ -10,7 +10,7 @@ def __init__(self, main_window):
# Load the .ui file
try:
- uic.loadUi('src/ui/loading_screen/loading_screen.ui', self)
+ uic.loadUi('ui/loading_screen/loading_screen.ui', self)
print("UI file loaded successfully")
except Exception as e:
print(f"Failed to load UI file: {e}")
diff --git a/src/ui/main_window.py b/src/ui/main_window.py
index 89738292..aa3de4a0 100644
--- a/src/ui/main_window.py
+++ b/src/ui/main_window.py
@@ -4,9 +4,12 @@
from config import Config
from models.printer_status import PrinterStatus
from PyQt5.QtCore import QTimer
-from temperatureController.chamberTemperatureController import ChamberTemperatureController # Ensure this import is present
-from Feeltek.scanCard import Scancard # Import Scancard
+from temperatureController.chamberTemperatureController import ChamberTemperatureController
+from Feeltek.scanCard import Scancard
from processAutomationController.processAutomationController import ProcessAutomationController
+from layerManager.layerQueueManager import LayerQueueManager
+from multiLayerPrintController import MultiLayerPrintController
+from layerManager.printStateManager import PrintStateManager
from utils.helpers import run_async
if not Config.DEVELOPMENT_MODE:
@@ -15,15 +18,17 @@
from rgbCamera.rgbCamera import RGBCamera
from moonrakerClient.moonrakerClient import MoonrakerAPI
-import ui.resources.resource_rc # Ensure resources are loaded
+import ui.resources.resource_rc
import traceback
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
- self.printer_status = PrinterStatus() # Create an instance of the PrinterStatus model
- self.process_automation_controller = ProcessAutomationController(self) # Initialize ProcessAutomationController
+ self.printer_status = PrinterStatus()
+ self.process_automation_controller = ProcessAutomationController(self)
+
+
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
@@ -37,7 +42,7 @@ def __init__(self):
if not Config.DEVELOPMENT_MODE:
self.thermal_camera = ThermalCamera(roi=(2, 13, 59, 64))
self.thermal_camera.thermal_camera_frame_ready.connect(self.update_frame)
- self.thermal_camera.max_temp_signal.connect(self.update_max_temp) # Connect max_temp_signal to update_max_temp
+ self.thermal_camera.max_temp_signal.connect(self.update_max_temp)
self.thermal_camera.start()
self.rgb_camera = RGBCamera()
@@ -47,36 +52,35 @@ def __init__(self):
self.thermal_camera = None
self.rgb_camera = None
- # Initialize HeaterBoard and ChamberTemperatureController if not in development mode
if not Config.DEVELOPMENT_MODE:
self.chamber_temp_controller = ChamberTemperatureController(self.printer_status)
else:
self.chamber_temp_controller = None
- # Initialize MoonrakerAPI if not in development mode
if not Config.DEVELOPMENT_MODE:
self.moonraker_api = MoonrakerAPI('http://10.20.1.135')
else:
self.moonraker_api = MockMoonrakerAPI()
- # Initialize Scancard
self.scancard = Scancard(self) if not Config.DEVELOPMENT_MODE else MockScancard(self)
- # Set up a QTimer to periodically check the Scancard status
self.scancard_timer = QTimer(self)
self.scancard_timer.timeout.connect(self.handle_scancard_status_change)
- self.scancard_timer.start(5000) # Check status every 5000 ms (5 seconds)
+ self.scancard_timer.start(5000)
- # Load sub UIs based on configuration
self.load_loading_screen()
self.load_tab_screen()
self.switch_screen(self.loading_screen)
- # Adjust the size of the main window to fit its contents
self.adjustSize()
self.process_automation_controller.progress_update_signal.connect(self.update_progress_bar)
+ # Initialize multi-layer printing components
+ self.layer_queue_manager = LayerQueueManager()
+ self.print_state_manager = PrintStateManager()
+ self.multi_layer_controller = MultiLayerPrintController(self)
+
def update_progress_bar(self, value):
self.home_screen.printProgressBar.setValue(value)
self.control_screen.recoaterProgressBar.setValue(value)
@@ -92,14 +96,13 @@ def load_tab_screen(self):
def switch_screen(self, widget):
print(f"Switching to screen: {widget}")
self.stacked_widget.setCurrentWidget(widget)
- self.adjustSize() # Adjust size after switching screens
+ self.adjustSize()
def switch_to_tab_screen(self):
self.switch_screen(self.tab_screen)
def update_frame(self, frame, chamberTemperatures):
if frame is not None and chamberTemperatures is not None:
- # Convert temps values to regular float
converted_temps = {key: float(value) for key, value in chamberTemperatures.items()}
self.printer_status.updateTemperatures(frame, converted_temps)
@@ -110,7 +113,6 @@ def update_rgb_frame(self, frame):
if frame is not None:
self.printer_status.updateRGBFrame(frame)
- # Add methods to interact with Scancard
def start_scancard_mark(self):
self.scancard.start_mark()
@@ -127,7 +129,6 @@ def update_scancard_status(self, future):
status = future.result()
self.printer_status.updateScancardStatus(status)
self.control_screen.scanCardStatusLabel.setText("Status: " + self.printer_status.scancard_status)
- # print(f"Scancard status: {self.printer_status.scancard_status}")
except Exception as e:
print(f"Failed to update Scancard status: {e}")
@@ -175,6 +176,14 @@ def _get_scancard_return_meaning(self, ret_value):
def update_file_info_label(self, file_path: str):
self.home_screen.fileInfoLabel.setText(file_path)
+
+ def resume_print_from_saved_state(self, state_file):
+ """Resume a print from a saved state file."""
+ if self.multi_layer_controller.load_print_state(state_file):
+ self.multi_layer_controller.resume_multi_layer_print()
+ else:
+ print("Failed to load print state")
+
class MockMoonrakerAPI:
def __init__(self):
@@ -191,15 +200,18 @@ def query_temperatures(self):
print("MockMoonrakerAPI.query_temperatures called")
return {"temperatures": "mock_temperatures"}
+
class MockScancard:
def __init__(self, main_window):
print("MockScancard initialized")
def start_mark(self):
print("MockScancard.start_mark called")
+ return MockFuture()
def stop_mark(self):
print("MockScancard.stop_mark called")
+ return MockFuture()
def get_working_status(self):
print("MockScancard.get_working_status called")
@@ -213,6 +225,7 @@ def close_file(self):
print("MockScancard.close_file called")
return MockFuture()
+
class MockFuture:
def add_done_callback(self, callback):
print("MockFuture.add_done_callback called")
@@ -220,6 +233,4 @@ def add_done_callback(self, callback):
def result(self):
print("MockFuture.result called")
- return {"ret_value": 1} # Simulated response
-
-
+ return {"ret_value": 1} # Simulated response
\ No newline at end of file
diff --git a/src/ui/parameters_screen/parameters_screen.py b/src/ui/parameters_screen/parameters_screen.py
index 129085e7..dba93f8e 100644
--- a/src/ui/parameters_screen/parameters_screen.py
+++ b/src/ui/parameters_screen/parameters_screen.py
@@ -9,7 +9,7 @@ def __init__(self, main_window):
self.printer_status = main_window.printer_status # Assuming main_window has a printer_status attribute
try:
- uic.loadUi('src/ui/parameters_screen/parameters_screen.ui', self)
+ uic.loadUi('ui/parameters_screen/parameters_screen.ui', self)
print("ParametersScreen UI loaded successfully")
except Exception as e:
print(f"Failed to load ParametersScreen UI: {e}")
diff --git a/src/ui/tab_screen/tab_screen.py b/src/ui/tab_screen/tab_screen.py
index 612c7834..d3c54285 100644
--- a/src/ui/tab_screen/tab_screen.py
+++ b/src/ui/tab_screen/tab_screen.py
@@ -10,7 +10,7 @@ def __init__(self, main_window):
self.main_window = main_window
# Load the .ui file for tab screen
try:
- uic.loadUi('src/ui/tab_screen/tab_screen.ui', self)
+ uic.loadUi('ui/tab_screen/tab_screen.ui', self)
print("TabScreen UI loaded successfully")
except Exception as e:
print(f"Failed to load TabScreen UI: {e}")