import re import os import logging from typing import Dict, Any, Optional from config import settings logger = logging.getLogger(__name__) class GCodeParser: @staticmethod def parse_metadata(gcode_path: str) -> Dict[str, Any]: """ Parses the G-code to extract estimated time and material usage. Scans both the beginning (header) and end (footer) of the file. """ stats = { "print_time_seconds": 0, "filament_length_mm": 0, "filament_volume_mm3": 0, "filament_weight_g": 0, "slicer_estimated_cost": 0 } if not os.path.exists(gcode_path): logger.warning(f"GCode file not found for parsing: {gcode_path}") return stats try: with open(gcode_path, 'r', encoding='utf-8', errors='ignore') as f: # Read header (first 500 lines) header_lines = [f.readline().strip() for _ in range(500) if f] # Read footer (last 20KB) f.seek(0, 2) file_size = f.tell() read_len = min(file_size, 20480) f.seek(file_size - read_len) footer_lines = f.read().splitlines() all_lines = header_lines + footer_lines for line in all_lines: line = line.strip() if not line.startswith(";"): continue GCodeParser._parse_line(line, stats) # Fallback calculation if stats["filament_weight_g"] == 0 and stats["filament_length_mm"] > 0: GCodeParser._calculate_weight_fallback(stats) except Exception as e: logger.error(f"Error parsing G-code: {e}") return stats @staticmethod def _parse_line(line: str, stats: Dict[str, Any]): # Parse Time if "estimated printing time =" in line: # Header time_str = line.split("=")[1].strip() stats["print_time_seconds"] = GCodeParser._parse_time_string(time_str) elif "total estimated time:" in line: # Footer parts = line.split("total estimated time:") if len(parts) > 1: stats["print_time_seconds"] = GCodeParser._parse_time_string(parts[1].strip()) # Parse Filament info if "filament used [g] =" in line: try: stats["filament_weight_g"] = float(line.split("=")[1].strip()) except ValueError: pass if "filament used [mm] =" in line: try: stats["filament_length_mm"] = float(line.split("=")[1].strip()) except ValueError: pass if "filament used [cm3] =" in line: try: # cm3 to mm3 stats["filament_volume_mm3"] = float(line.split("=")[1].strip()) * 1000 except ValueError: pass @staticmethod def _calculate_weight_fallback(stats: Dict[str, Any]): # Assumes 1.75mm diameter and PLA density 1.24 radius = 1.75 / 2 volume_mm3 = 3.14159 * (radius ** 2) * stats["filament_length_mm"] volume_cm3 = volume_mm3 / 1000.0 stats["filament_weight_g"] = volume_cm3 * 1.24 @staticmethod def _parse_time_string(time_str: str) -> int: """ Converts '1d 2h 3m 4s' to seconds. """ total_seconds = 0 days = re.search(r'(\d+)d', time_str) hours = re.search(r'(\d+)h', time_str) mins = re.search(r'(\d+)m', time_str) secs = re.search(r'(\d+)s', time_str) if days: total_seconds += int(days.group(1)) * 86400 if hours: total_seconds += int(hours.group(1)) * 3600 if mins: total_seconds += int(mins.group(1)) * 60 if secs: total_seconds += int(secs.group(1)) return total_seconds class QuoteCalculator: @staticmethod def calculate(stats: Dict[str, Any]) -> Dict[str, Any]: """ Calculates the final quote based on parsed stats and settings. """ # 1. Material Cost # Cost per gram = (Cost per kg / 1000) material_cost = (stats["filament_weight_g"] / 1000.0) * settings.FILAMENT_COST_PER_KG # 2. Machine Time Cost # Cost per second = (Cost per hour / 3600) print_time_hours = stats["print_time_seconds"] / 3600.0 machine_cost = print_time_hours * settings.MACHINE_COST_PER_HOUR # 3. Energy Cost # kWh = (Watts / 1000) * hours kwh_used = (settings.PRINTER_POWER_WATTS / 1000.0) * print_time_hours energy_cost = kwh_used * settings.ENERGY_COST_PER_KWH # Subtotal subtotal = material_cost + machine_cost + energy_cost # 4. Markup markup_factor = 1.0 + (settings.MARKUP_PERCENT / 100.0) total_price = subtotal * markup_factor return { "breakdown": { "material_cost": round(material_cost, 2), "machine_cost": round(machine_cost, 2), "energy_cost": round(energy_cost, 2), "subtotal": round(subtotal, 2), "markup_amount": round(total_price - subtotal, 2) }, "total_price": round(total_price, 2), "currency": "EUR" }