# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2016 sliptonic * # * * # * This file is part of the FreeCAD CAx development system. * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * FreeCAD is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with FreeCAD; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** """ This file has utilities for checking and catching common errors in FreeCAD CAM projects. Ideally, the user could execute these utilities from an icon to make sure tools are selected and configured and defaults have been revised """ from collections import Counter from datetime import datetime import FreeCAD import Path import Path.Log import Path.Main.Sanity.ImageBuilder as ImageBuilder import Path.Main.Sanity.ReportGenerator as ReportGenerator import os import tempfile import Path.Dressup.Utils as PathDressup translate = FreeCAD.Qt.translate if False: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) class CAMSanity: """ This class has the functionality to harvest data from a CAM Job and export it in a format that is useful to the user. """ def __init__(self, job, output_file): self.job = job self.output_file = output_file self.filelocation = os.path.dirname(output_file) # set the filelocation to the parent of the output filename if not os.path.isdir(self.filelocation): raise ValueError( translate( "CAM_Sanity", "output location {} doesn't exist".format(os.path.dirname(output_file)), ) ) self.image_builder = ImageBuilder.ImageBuilderFactory.get_image_builder(self.filelocation) self.data = self.summarize() def summarize(self): """ Gather all the incremental parts of the analysis """ data = {} data["baseData"] = self._baseObjectData() data["designData"] = self._designData() data["toolData"] = self._toolData() data["runData"] = self._runData() data["outputData"] = self._outputData() data["fixtureData"] = self._fixtureData() data["stockData"] = self._stockData() # data["squawkData"] = self._squawkData() return data def squawk(self, operator, note, date=datetime.now(), squawkType="NOTE"): squawkType = squawkType if squawkType in ("NOTE", "WARNING", "CAUTION", "TIP") else "NOTE" if squawkType == "TIP": squawk_icon = "Sanity_Bulb" elif squawkType == "NOTE": squawk_icon = "Sanity_Note" elif squawkType == "WARNING": squawk_icon = "Sanity_Warning" elif squawkType == "CAUTION": squawk_icon = "Sanity_Caution" path = f"{FreeCAD.getHomePath()}Mod/CAM/Path/Main/Sanity/{squawk_icon}.svg" squawk = { "Date": str(date), "Operator": operator, "Note": note, "squawkType": squawkType, "squawkIcon": path, } return squawk def _baseObjectData(self): data = {"baseimage": "", "bases": "", "squawkData": []} obj = self.job bases = {} for name, count in Counter( [obj.Proxy.baseObject(obj, o).Label for o in obj.Model.Group] ).items(): bases[name] = str(count) data["baseimage"] = self.image_builder.build_image(obj.Model, "baseimage") data["bases"] = bases return data def _designData(self): """ Returns header information about the design document Returns information about issues and concerns (squawks) """ obj = self.job data = { "FileName": "", "LastModifiedDate": "", "Customer": "", "Designer": "", "JobDescription": "", "JobLabel": "", "Sequence": "", "JobType": "", "squawkData": [], } data["FileName"] = obj.Document.FileName data["LastModifiedDate"] = str(obj.Document.LastModifiedDate) data["Customer"] = obj.Document.Company data["Designer"] = obj.Document.LastModifiedBy data["JobDescription"] = obj.Description data["JobLabel"] = obj.Label n = 0 m = 0 for i in obj.Document.Objects: if hasattr(i, "Proxy"): if isinstance(i.Proxy, Path.Main.Job.ObjectJob): m += 1 if i is obj: n = m data["Sequence"] = "{} of {}".format(n, m) data["JobType"] = "2.5D Milling" # improve after job types added return data def _fixtureData(self): obj = self.job data = {"fixtures": "", "orderBy": "", "datumImage": "", "squawkData": []} data["fixtures"] = str(obj.Fixtures) data["orderBy"] = str(obj.OrderOutputBy) data["datumImage"] = self.image_builder.build_image(obj, "datumImage") return data def _outputData(self): obj = self.job data = { "lastpostprocess": "", "lastgcodefile": "", "optionalstops": "", "programmer": "", "machine": "", "postprocessor": "", "postprocessorFlags": "", "filesize": "", "linecount": "", "outputfilename": "setupreport", "squawkData": [], } data["lastpostprocess"] = str(obj.LastPostProcessDate) data["lastgcodefile"] = str(obj.LastPostProcessOutput) data["optionalstops"] = "False" data["programmer"] = "" data["machine"] = "" data["postprocessor"] = str(obj.PostProcessor) data["postprocessorFlags"] = str(obj.PostProcessorArgs) if obj.PostProcessorOutputFile != "": fname = obj.PostProcessorOutputFile data["outputfilename"] = os.path.splitext(os.path.basename(fname))[0] for op in obj.Operations.Group: if "Stop" in op.Name and hasattr(op, "Stop") and op.Stop is True: data["optionalstops"] = "True" if obj.LastPostProcessOutput == "": data["filesize"] = str(0.0) data["linecount"] = str(0) data["squawkData"].append( self.squawk( "CAMSanity", translate("CAM_Sanity", "The Job has not been post-processed"), ) ) else: if os.path.isfile(obj.LastPostProcessOutput): data["filesize"] = str(os.path.getsize(obj.LastPostProcessOutput) / 1000) data["linecount"] = str(sum(1 for line in open(obj.LastPostProcessOutput))) else: data["filesize"] = str(0.0) data["linecount"] = str(0) data["squawkData"].append( self.squawk( "CAMSanity", translate( "CAM_Sanity", "The Job's last post-processed file is missing", ), ) ) return data def _runData(self): obj = self.job data = { "cycletotal": "", "jobMinZ": "", "jobMaxZ": "", "jobDescription": "", "operations": [], "squawkData": [], } data["cycletotal"] = str(obj.CycleTime) data["jobMinZ"] = FreeCAD.Units.Quantity( obj.Path.BoundBox.ZMin, FreeCAD.Units.Length ).UserString data["jobMaxZ"] = FreeCAD.Units.Quantity( obj.Path.BoundBox.ZMax, FreeCAD.Units.Length ).UserString data["jobDescription"] = obj.Description data["operations"] = [] for op in obj.Operations.Group: oplabel = op.Label Path.Log.debug(oplabel) ctime = op.CycleTime if hasattr(op, "CycleTime") else "00:00:00" cool = op.CoolantMode if hasattr(op, "CoolantMode") else "N/A" o = op while "Dressup" in o.Name: oplabel = "{}:{}".format(oplabel, o.Base.Label) o = o.Base if hasattr(o, "CycleTime"): ctime = o.CycleTime cool = o.CoolantMode if hasattr(o, "CoolantMode") else cool if hasattr(op, "Active") and not op.Active: oplabel = "{} (INACTIVE)".format(oplabel) ctime = "00:00:00" if op.Path.BoundBox.isValid(): zmin = FreeCAD.Units.Quantity( op.Path.BoundBox.ZMin, FreeCAD.Units.Length ).UserString zmax = FreeCAD.Units.Quantity( op.Path.BoundBox.ZMax, FreeCAD.Units.Length ).UserString else: zmin = "" zmax = "" opdata = { "opName": oplabel, "minZ": zmin, "maxZ": zmax, "cycleTime": ctime, "coolantMode": cool, } data["operations"].append(opdata) return data def _stockData(self): obj = self.job data = { "xLen": "", "yLen": "", "zLen": "", "material": "", "surfaceSpeedCarbide": "", "surfaceSpeedHSS": "", "stockImage": "", "squawkData": [], } bb = obj.Stock.Shape.BoundBox data["xLen"] = FreeCAD.Units.Quantity(bb.XLength, FreeCAD.Units.Length).UserString data["yLen"] = FreeCAD.Units.Quantity(bb.YLength, FreeCAD.Units.Length).UserString data["zLen"] = FreeCAD.Units.Quantity(bb.ZLength, FreeCAD.Units.Length).UserString data["material"] = "Not Specified" if hasattr(obj.Stock, "ShapeMaterial"): if obj.Stock.ShapeMaterial is not None: data["material"] = obj.Stock.ShapeMaterial.Name props = obj.Stock.ShapeMaterial.PhysicalProperties if "SurfaceSpeedCarbide" in props: data["surfaceSpeedCarbide"] = FreeCAD.Units.Quantity( props["SurfaceSpeedCarbide"] ).UserString if "SurfaceSpeedHSS" in props: data["surfaceSpeedHSS"] = FreeCAD.Units.Quantity( props["SurfaceSpeedHSS"] ).UserString if data["material"] in ["Default", "Not Specified"]: data["squawkData"].append( self.squawk( "CAMSanity", translate("CAM_Sanity", "Consider Specifying the Stock Material"), squawkType="TIP", ) ) data["stockImage"] = self.image_builder.build_image(obj.Stock, "stockImage") return data def _toolData(self): """ Returns information about the tools used in the job, and associated toolcontrollers Returns information about issues and problems with the tools (squawks) """ obj = self.job data = {"squawkData": []} for TC in obj.Tools.Group: if not hasattr(TC.Tool, "BitBody"): data["squawkData"].append( data["squawkData"].append( self.squawk( "CAMSanity", translate( "CAM_Sanity", "Tool number {} is a legacy tool. Legacy tools not \ supported by Path-Sanity", ).format(TC.ToolNumber), squawkType="WARNING", ) ) ) continue # skip old-style tools tooldata = data.setdefault(str(TC.ToolNumber), {}) bitshape = tooldata.setdefault("BitShape", "") if bitshape not in ["", TC.Tool.BitShape]: data["squawkData"].append( self.squawk( "CAMSanity", translate("CAM_Sanity", "Tool number {} used by multiple tools").format( TC.ToolNumber ), squawkType="CAUTION", ) ) tooldata["bitShape"] = TC.Tool.BitShape tooldata["description"] = TC.Tool.Label tooldata["manufacturer"] = "" tooldata["url"] = "" tooldata["inspectionNotes"] = "" tooldata["diameter"] = str(TC.Tool.Diameter) tooldata["shape"] = TC.Tool.ShapeName tooldata["partNumber"] = "" if os.path.isfile(TC.Tool.BitShape): imagedata = TC.Tool.Proxy.getBitThumbnail(TC.Tool) else: imagedata = None data["squawkData"].append( self.squawk( "CAMSanity", translate("CAM_Sanity", "Toolbit Shape for TC: {} not found").format( TC.ToolNumber ), squawkType="WARNING", ) ) tooldata["image"] = "" imagepath = os.path.join(self.filelocation, f"T{TC.ToolNumber}.png") tooldata["imagepath"] = imagepath Path.Log.debug(imagepath) if imagedata is not None: with open(imagepath, "wb") as fd: fd.write(imagedata) fd.close() tooldata["feedrate"] = str(TC.HorizFeed) if TC.HorizFeed.Value == 0.0: data["squawkData"].append( self.squawk( "CAMSanity", translate("CAM_Sanity", "Tool Controller '{}' has no feedrate").format( TC.Label ), squawkType="WARNING", ) ) tooldata["spindlespeed"] = str(TC.SpindleSpeed) if TC.SpindleSpeed == 0.0: data["squawkData"].append( self.squawk( "CAMSanity", translate("CAM_Sanity", "Tool Controller '{}' has no spindlespeed").format( TC.Label ), squawkType="WARNING", ) ) used = False for op in obj.Operations.Group: base_op = PathDressup.baseOp(op) if hasattr(base_op, "ToolController") and base_op.ToolController is TC: used = True tooldata.setdefault("ops", []).append( { "Operation": base_op.Label, "ToolController": TC.Label, "Feed": str(TC.HorizFeed), "Speed": str(TC.SpindleSpeed), } ) if used is False: tooldata.setdefault("ops", []) data["squawkData"].append( self.squawk( "CAMSanity", translate("CAM_Sanity", "Tool Controller '{}' is not used").format( TC.Label ), squawkType="WARNING", ) ) return data def serialize(self, obj): """A function to serialize non-serializable objects.""" if isinstance(obj, type(Exception)): # Convert an exception to its string representation return str(obj) # You might need to handle more types depending on your needs return str(obj) # Fallback to convert any other non-serializable types to string def get_output_report(self): Path.Log.debug("get_output_url") generator = ReportGenerator.ReportGenerator(self.data, embed_images=True) html = generator.generate_html() generator = None return html