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