Files
create/src/Mod/CAM/Path/Main/Sanity/Sanity.py
2025-05-19 20:27:28 +02:00

481 lines
17 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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("ShapeType", "")
if bitshape not in ["", TC.Tool.ShapeType]:
data["squawkData"].append(
self.squawk(
"CAMSanity",
translate("CAM_Sanity", "Tool number {} used by multiple tools").format(
TC.ToolNumber
),
squawkType="CAUTION",
)
)
tooldata["bitShape"] = TC.Tool.ShapeType
tooldata["description"] = TC.Tool.Label
tooldata["manufacturer"] = ""
tooldata["url"] = ""
tooldata["inspectionNotes"] = ""
tooldata["diameter"] = str(TC.Tool.Diameter)
tooldata["shape"] = TC.Tool.ShapeType
tooldata["partNumber"] = ""
if os.path.isfile(TC.Tool.ShapeType):
imagedata = TC.Tool.Proxy.get_thumbnail()
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