0321 from macmini
This commit is contained in:
843
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad.py
vendored
Normal file
843
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad.py
vendored
Normal file
@ -0,0 +1,843 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import pcbnew
|
||||
|
||||
from .common import EcadParser, Component, ExtraFieldData
|
||||
from .kicad_extra import find_latest_schematic_data, parse_schematic_data
|
||||
from .svgpath import create_path
|
||||
from ..core import ibom
|
||||
from ..core.config import Config
|
||||
from ..core.fontparser import FontParser
|
||||
|
||||
|
||||
class PcbnewParser(EcadParser):
|
||||
|
||||
def __init__(self, file_name, config, logger, board=None):
|
||||
super(PcbnewParser, self).__init__(file_name, config, logger)
|
||||
self.board = board
|
||||
if self.board is None:
|
||||
self.board = pcbnew.LoadBoard(self.file_name) # type: pcbnew.BOARD
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
self.footprints = list(self.board.GetModules())
|
||||
else:
|
||||
self.footprints = list(self.board.GetFootprints())
|
||||
self.font_parser = FontParser()
|
||||
|
||||
def get_extra_field_data(self, file_name):
|
||||
if os.path.abspath(file_name) == os.path.abspath(self.file_name):
|
||||
return self.parse_extra_data_from_pcb()
|
||||
if os.path.splitext(file_name)[1] == '.kicad_pcb':
|
||||
return None
|
||||
|
||||
data = parse_schematic_data(file_name)
|
||||
|
||||
return ExtraFieldData(data[0], data[1])
|
||||
|
||||
@staticmethod
|
||||
def get_footprint_fields(f):
|
||||
# type: (pcbnew.FOOTPRINT) -> dict
|
||||
props = {}
|
||||
if hasattr(f, "GetProperties"):
|
||||
props = f.GetProperties()
|
||||
if hasattr(f, "GetFields"):
|
||||
props = f.GetFieldsShownText()
|
||||
if "dnp" in props and props["dnp"] == "":
|
||||
del props["dnp"]
|
||||
props["kicad_dnp"] = "DNP"
|
||||
if hasattr(f, "IsDNP"):
|
||||
if f.IsDNP():
|
||||
props["kicad_dnp"] = "DNP"
|
||||
return props
|
||||
|
||||
def parse_extra_data_from_pcb(self):
|
||||
field_set = set()
|
||||
by_ref = {}
|
||||
by_index = {}
|
||||
|
||||
for (i, f) in enumerate(self.footprints):
|
||||
props = self.get_footprint_fields(f)
|
||||
by_index[i] = props
|
||||
ref = f.GetReference()
|
||||
ref_fields = by_ref.setdefault(ref, {})
|
||||
|
||||
for k, v in props.items():
|
||||
field_set.add(k)
|
||||
ref_fields[k] = v
|
||||
|
||||
return ExtraFieldData(list(field_set), by_ref, by_index)
|
||||
|
||||
def latest_extra_data(self, extra_dirs=None):
|
||||
base_name = os.path.splitext(os.path.basename(self.file_name))[0]
|
||||
extra_dirs.append(self.board.GetPlotOptions().GetOutputDirectory())
|
||||
file_dir_name = os.path.dirname(self.file_name)
|
||||
directories = [file_dir_name]
|
||||
for dir in extra_dirs:
|
||||
if not os.path.isabs(dir):
|
||||
dir = os.path.join(file_dir_name, dir)
|
||||
if os.path.exists(dir):
|
||||
directories.append(dir)
|
||||
return find_latest_schematic_data(base_name, directories)
|
||||
|
||||
def extra_data_file_filter(self):
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
return "Netlist and xml files (*.net; *.xml)|*.net;*.xml"
|
||||
else:
|
||||
return ("Netlist, xml and pcb files (*.net; *.xml; *.kicad_pcb)|"
|
||||
"*.net;*.xml;*.kicad_pcb")
|
||||
|
||||
@staticmethod
|
||||
def normalize(point):
|
||||
return [point.x * 1e-6, point.y * 1e-6]
|
||||
|
||||
@staticmethod
|
||||
def normalize_angle(angle):
|
||||
if isinstance(angle, int) or isinstance(angle, float):
|
||||
return angle * 0.1
|
||||
else:
|
||||
return angle.AsDegrees()
|
||||
|
||||
def get_arc_angles(self, d):
|
||||
# type: (pcbnew.PCB_SHAPE) -> tuple
|
||||
a1 = self.normalize_angle(d.GetArcAngleStart())
|
||||
if hasattr(d, "GetAngle"):
|
||||
a2 = a1 + self.normalize_angle(d.GetAngle())
|
||||
else:
|
||||
a2 = a1 + self.normalize_angle(d.GetArcAngle())
|
||||
if a2 < a1:
|
||||
a1, a2 = a2, a1
|
||||
return round(a1, 2), round(a2, 2)
|
||||
|
||||
def parse_shape(self, d):
|
||||
# type: (pcbnew.PCB_SHAPE) -> dict or None
|
||||
shape = {
|
||||
pcbnew.S_SEGMENT: "segment",
|
||||
pcbnew.S_CIRCLE: "circle",
|
||||
pcbnew.S_ARC: "arc",
|
||||
pcbnew.S_POLYGON: "polygon",
|
||||
pcbnew.S_CURVE: "curve",
|
||||
pcbnew.S_RECT: "rect",
|
||||
}.get(d.GetShape(), "")
|
||||
if shape == "":
|
||||
self.logger.info("Unsupported shape %s, skipping", d.GetShape())
|
||||
return None
|
||||
start = self.normalize(d.GetStart())
|
||||
end = self.normalize(d.GetEnd())
|
||||
if shape == "segment":
|
||||
return {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"end": end,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
|
||||
if shape == "rect":
|
||||
if hasattr(d, "GetRectCorners"):
|
||||
points = list(map(self.normalize, d.GetRectCorners()))
|
||||
else:
|
||||
points = [
|
||||
start,
|
||||
[end[0], start[1]],
|
||||
end,
|
||||
[start[0], end[1]]
|
||||
]
|
||||
shape_dict = {
|
||||
"type": "polygon",
|
||||
"pos": [0, 0],
|
||||
"angle": 0,
|
||||
"polygons": [points],
|
||||
"width": d.GetWidth() * 1e-6,
|
||||
"filled": 0
|
||||
}
|
||||
if hasattr(d, "IsFilled") and d.IsFilled():
|
||||
shape_dict["filled"] = 1
|
||||
return shape_dict
|
||||
|
||||
if shape == "circle":
|
||||
shape_dict = {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"radius": d.GetRadius() * 1e-6,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
if hasattr(d, "IsFilled") and d.IsFilled():
|
||||
shape_dict["filled"] = 1
|
||||
return shape_dict
|
||||
|
||||
if shape == "arc":
|
||||
a1, a2 = self.get_arc_angles(d)
|
||||
if hasattr(d, "GetCenter"):
|
||||
start = self.normalize(d.GetCenter())
|
||||
return {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"radius": d.GetRadius() * 1e-6,
|
||||
"startangle": a1,
|
||||
"endangle": a2,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
|
||||
if shape == "polygon":
|
||||
if hasattr(d, "GetPolyShape"):
|
||||
polygons = self.parse_poly_set(d.GetPolyShape())
|
||||
else:
|
||||
self.logger.info(
|
||||
"Polygons not supported for KiCad 4, skipping")
|
||||
return None
|
||||
angle = 0
|
||||
if hasattr(d, 'GetParentModule'):
|
||||
parent_footprint = d.GetParentModule()
|
||||
else:
|
||||
parent_footprint = d.GetParentFootprint()
|
||||
if parent_footprint is not None:
|
||||
angle = self.normalize_angle(parent_footprint.GetOrientation())
|
||||
shape_dict = {
|
||||
"type": shape,
|
||||
"pos": start,
|
||||
"angle": angle,
|
||||
"polygons": polygons
|
||||
}
|
||||
if hasattr(d, "IsFilled") and not d.IsFilled():
|
||||
shape_dict["filled"] = 0
|
||||
shape_dict["width"] = d.GetWidth() * 1e-6
|
||||
return shape_dict
|
||||
if shape == "curve":
|
||||
if hasattr(d, "GetBezierC1"):
|
||||
c1 = self.normalize(d.GetBezierC1())
|
||||
c2 = self.normalize(d.GetBezierC2())
|
||||
else:
|
||||
c1 = self.normalize(d.GetBezControl1())
|
||||
c2 = self.normalize(d.GetBezControl2())
|
||||
return {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"cpa": c1,
|
||||
"cpb": c2,
|
||||
"end": end,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
|
||||
def parse_line_chain(self, shape):
|
||||
# type: (pcbnew.SHAPE_LINE_CHAIN) -> list
|
||||
result = []
|
||||
if not hasattr(shape, "PointCount"):
|
||||
self.logger.warn("No PointCount method on outline object. "
|
||||
"Unpatched kicad version?")
|
||||
return result
|
||||
|
||||
for point_index in range(shape.PointCount()):
|
||||
result.append(
|
||||
self.normalize(shape.CPoint(point_index)))
|
||||
|
||||
return result
|
||||
|
||||
def parse_poly_set(self, poly):
|
||||
# type: (pcbnew.SHAPE_POLY_SET) -> list
|
||||
result = []
|
||||
|
||||
for i in range(poly.OutlineCount()):
|
||||
result.append(self.parse_line_chain(poly.Outline(i)))
|
||||
|
||||
return result
|
||||
|
||||
def parse_text(self, d):
|
||||
# type: (pcbnew.PCB_TEXT) -> dict
|
||||
if not d.IsVisible() and d.GetClass() not in ["PTEXT", "PCB_TEXT"]:
|
||||
return None
|
||||
pos = self.normalize(d.GetPosition())
|
||||
if hasattr(d, "GetTextThickness"):
|
||||
thickness = d.GetTextThickness() * 1e-6
|
||||
else:
|
||||
thickness = d.GetThickness() * 1e-6
|
||||
if hasattr(d, 'TransformToSegmentList'):
|
||||
segments = [self.normalize(p) for p in d.TransformToSegmentList()]
|
||||
lines = []
|
||||
for i in range(0, len(segments), 2):
|
||||
if i == 0 or segments[i - 1] != segments[i]:
|
||||
lines.append([segments[i]])
|
||||
lines[-1].append(segments[i + 1])
|
||||
return {
|
||||
"thickness": thickness,
|
||||
"svgpath": create_path(lines)
|
||||
}
|
||||
elif hasattr(d, 'GetEffectiveTextShape'):
|
||||
shape = d.GetEffectiveTextShape(False) # type: pcbnew.SHAPE_COMPOUND
|
||||
segments = []
|
||||
polygons = []
|
||||
for s in shape.GetSubshapes():
|
||||
if s.Type() == pcbnew.SH_LINE_CHAIN:
|
||||
polygons.append(self.parse_line_chain(s))
|
||||
elif s.Type() == pcbnew.SH_SEGMENT:
|
||||
seg = s.GetSeg()
|
||||
segments.append(
|
||||
[self.normalize(seg.A), self.normalize(seg.B)])
|
||||
else:
|
||||
self.logger.warn(
|
||||
"Unsupported subshape in text: %s" % s.Type())
|
||||
if segments:
|
||||
return {
|
||||
"thickness": thickness,
|
||||
"svgpath": create_path(segments)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"polygons": polygons
|
||||
}
|
||||
|
||||
if d.GetClass() == "MTEXT":
|
||||
angle = self.normalize_angle(d.GetDrawRotation())
|
||||
else:
|
||||
if hasattr(d, "GetTextAngle"):
|
||||
angle = self.normalize_angle(d.GetTextAngle())
|
||||
else:
|
||||
angle = self.normalize_angle(d.GetOrientation())
|
||||
if hasattr(d, "GetTextHeight"):
|
||||
height = d.GetTextHeight() * 1e-6
|
||||
width = d.GetTextWidth() * 1e-6
|
||||
else:
|
||||
height = d.GetHeight() * 1e-6
|
||||
width = d.GetWidth() * 1e-6
|
||||
if hasattr(d, "GetShownText"):
|
||||
text = d.GetShownText()
|
||||
else:
|
||||
text = d.GetText()
|
||||
self.font_parser.parse_font_for_string(text)
|
||||
attributes = []
|
||||
if d.IsMirrored():
|
||||
attributes.append("mirrored")
|
||||
if d.IsItalic():
|
||||
attributes.append("italic")
|
||||
if d.IsBold():
|
||||
attributes.append("bold")
|
||||
|
||||
return {
|
||||
"pos": pos,
|
||||
"text": text,
|
||||
"height": height,
|
||||
"width": width,
|
||||
"justify": [d.GetHorizJustify(), d.GetVertJustify()],
|
||||
"thickness": thickness,
|
||||
"attr": attributes,
|
||||
"angle": angle
|
||||
}
|
||||
|
||||
def parse_dimension(self, d):
|
||||
# type: (pcbnew.PCB_DIMENSION_BASE) -> dict
|
||||
segments = []
|
||||
circles = []
|
||||
for s in d.GetShapes():
|
||||
s = s.Cast()
|
||||
if s.Type() == pcbnew.SH_SEGMENT:
|
||||
seg = s.GetSeg()
|
||||
segments.append(
|
||||
[self.normalize(seg.A), self.normalize(seg.B)])
|
||||
elif s.Type() == pcbnew.SH_CIRCLE:
|
||||
circles.append(
|
||||
[self.normalize(s.GetCenter()), s.GetRadius() * 1e-6])
|
||||
else:
|
||||
self.logger.info(
|
||||
"Unsupported shape type in dimension object: %s", s.Type())
|
||||
|
||||
svgpath = create_path(segments, circles=circles)
|
||||
|
||||
return {
|
||||
"thickness": d.GetLineThickness() * 1e-6,
|
||||
"svgpath": svgpath
|
||||
}
|
||||
|
||||
def parse_drawing(self, d):
|
||||
# type: (pcbnew.BOARD_ITEM) -> list
|
||||
result = []
|
||||
s = None
|
||||
if d.GetClass() in ["DRAWSEGMENT", "MGRAPHIC", "PCB_SHAPE"]:
|
||||
s = self.parse_shape(d)
|
||||
elif d.GetClass() in ["PTEXT", "MTEXT", "FP_TEXT", "PCB_TEXT", "PCB_FIELD"]:
|
||||
s = self.parse_text(d)
|
||||
elif (d.GetClass().startswith("PCB_DIM")
|
||||
and hasattr(pcbnew, "VECTOR_SHAPEPTR")):
|
||||
result.append(self.parse_dimension(d))
|
||||
if hasattr(d, "Text"):
|
||||
s = self.parse_text(d.Text())
|
||||
else:
|
||||
s = self.parse_text(d)
|
||||
else:
|
||||
self.logger.info("Unsupported drawing class %s, skipping",
|
||||
d.GetClass())
|
||||
if s:
|
||||
result.append(s)
|
||||
return result
|
||||
|
||||
def parse_edges(self, pcb):
|
||||
edges = []
|
||||
drawings = list(pcb.GetDrawings())
|
||||
bbox = None
|
||||
for f in self.footprints:
|
||||
for g in f.GraphicalItems():
|
||||
drawings.append(g)
|
||||
for d in drawings:
|
||||
if d.GetLayer() == pcbnew.Edge_Cuts:
|
||||
for parsed_drawing in self.parse_drawing(d):
|
||||
edges.append(parsed_drawing)
|
||||
if bbox is None:
|
||||
bbox = d.GetBoundingBox()
|
||||
else:
|
||||
bbox.Merge(d.GetBoundingBox())
|
||||
if bbox:
|
||||
bbox.Normalize()
|
||||
return edges, bbox
|
||||
|
||||
def parse_drawings_on_layers(self, drawings, f_layer, b_layer):
|
||||
front = []
|
||||
back = []
|
||||
|
||||
for d in drawings:
|
||||
if d[1].GetLayer() not in [f_layer, b_layer]:
|
||||
continue
|
||||
for drawing in self.parse_drawing(d[1]):
|
||||
if d[0] in ["ref", "val"]:
|
||||
drawing[d[0]] = 1
|
||||
if d[1].GetLayer() == f_layer:
|
||||
front.append(drawing)
|
||||
else:
|
||||
back.append(drawing)
|
||||
|
||||
return {
|
||||
"F": front,
|
||||
"B": back
|
||||
}
|
||||
|
||||
def get_all_drawings(self):
|
||||
drawings = [(d.GetClass(), d) for d in list(self.board.GetDrawings())]
|
||||
for f in self.footprints:
|
||||
drawings.append(("ref", f.Reference()))
|
||||
drawings.append(("val", f.Value()))
|
||||
for d in f.GraphicalItems():
|
||||
drawings.append((d.GetClass(), d))
|
||||
return drawings
|
||||
|
||||
def parse_pad(self, pad):
|
||||
# type: (pcbnew.PAD) -> dict or None
|
||||
layers_set = list(pad.GetLayerSet().Seq())
|
||||
layers = []
|
||||
if pcbnew.F_Cu in layers_set:
|
||||
layers.append("F")
|
||||
if pcbnew.B_Cu in layers_set:
|
||||
layers.append("B")
|
||||
pos = self.normalize(pad.GetPosition())
|
||||
size = self.normalize(pad.GetSize())
|
||||
angle = self.normalize_angle(pad.GetOrientation())
|
||||
shape_lookup = {
|
||||
pcbnew.PAD_SHAPE_RECT: "rect",
|
||||
pcbnew.PAD_SHAPE_OVAL: "oval",
|
||||
pcbnew.PAD_SHAPE_CIRCLE: "circle",
|
||||
}
|
||||
if hasattr(pcbnew, "PAD_SHAPE_TRAPEZOID"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_TRAPEZOID] = "trapezoid"
|
||||
if hasattr(pcbnew, "PAD_SHAPE_ROUNDRECT"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_ROUNDRECT] = "roundrect"
|
||||
if hasattr(pcbnew, "PAD_SHAPE_CUSTOM"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_CUSTOM] = "custom"
|
||||
if hasattr(pcbnew, "PAD_SHAPE_CHAMFERED_RECT"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_CHAMFERED_RECT] = "chamfrect"
|
||||
shape = shape_lookup.get(pad.GetShape(), "")
|
||||
if shape == "":
|
||||
self.logger.info("Unsupported pad shape %s, skipping.",
|
||||
pad.GetShape())
|
||||
return None
|
||||
pad_dict = {
|
||||
"layers": layers,
|
||||
"pos": pos,
|
||||
"size": size,
|
||||
"angle": angle,
|
||||
"shape": shape
|
||||
}
|
||||
if shape == "custom":
|
||||
polygon_set = pad.GetCustomShapeAsPolygon()
|
||||
if polygon_set.HasHoles():
|
||||
self.logger.warn('Detected holes in custom pad polygons')
|
||||
pad_dict["polygons"] = self.parse_poly_set(polygon_set)
|
||||
if shape == "trapezoid":
|
||||
# treat trapezoid as custom shape
|
||||
pad_dict["shape"] = "custom"
|
||||
delta = self.normalize(pad.GetDelta())
|
||||
pad_dict["polygons"] = [[
|
||||
[size[0] / 2 + delta[1] / 2, size[1] / 2 - delta[0] / 2],
|
||||
[-size[0] / 2 - delta[1] / 2, size[1] / 2 + delta[0] / 2],
|
||||
[-size[0] / 2 + delta[1] / 2, -size[1] / 2 - delta[0] / 2],
|
||||
[size[0] / 2 - delta[1] / 2, -size[1] / 2 + delta[0] / 2],
|
||||
]]
|
||||
|
||||
if shape in ["roundrect", "chamfrect"]:
|
||||
pad_dict["radius"] = pad.GetRoundRectCornerRadius() * 1e-6
|
||||
if shape == "chamfrect":
|
||||
pad_dict["chamfpos"] = pad.GetChamferPositions()
|
||||
pad_dict["chamfratio"] = pad.GetChamferRectRatio()
|
||||
if hasattr(pcbnew, 'PAD_ATTRIB_PTH'):
|
||||
through_hole_attributes = [pcbnew.PAD_ATTRIB_PTH,
|
||||
pcbnew.PAD_ATTRIB_NPTH]
|
||||
else:
|
||||
through_hole_attributes = [pcbnew.PAD_ATTRIB_STANDARD,
|
||||
pcbnew.PAD_ATTRIB_HOLE_NOT_PLATED]
|
||||
if pad.GetAttribute() in through_hole_attributes:
|
||||
pad_dict["type"] = "th"
|
||||
pad_dict["drillshape"] = {
|
||||
pcbnew.PAD_DRILL_SHAPE_CIRCLE: "circle",
|
||||
pcbnew.PAD_DRILL_SHAPE_OBLONG: "oblong"
|
||||
}.get(pad.GetDrillShape())
|
||||
pad_dict["drillsize"] = self.normalize(pad.GetDrillSize())
|
||||
else:
|
||||
pad_dict["type"] = "smd"
|
||||
if hasattr(pad, "GetOffset"):
|
||||
pad_dict["offset"] = self.normalize(pad.GetOffset())
|
||||
if self.config.include_nets:
|
||||
pad_dict["net"] = pad.GetNetname()
|
||||
|
||||
return pad_dict
|
||||
|
||||
def parse_footprints(self):
|
||||
# type: () -> list
|
||||
footprints = []
|
||||
for f in self.footprints: # type: pcbnew.FOOTPRINT
|
||||
ref = f.GetReference()
|
||||
|
||||
# bounding box
|
||||
if hasattr(pcbnew, 'MODULE'):
|
||||
f_copy = pcbnew.MODULE(f)
|
||||
else:
|
||||
f_copy = pcbnew.FOOTPRINT(f)
|
||||
try:
|
||||
f_copy.SetOrientation(0)
|
||||
except TypeError:
|
||||
f_copy.SetOrientation(
|
||||
pcbnew.EDA_ANGLE(0, pcbnew.TENTHS_OF_A_DEGREE_T))
|
||||
pos = f_copy.GetPosition()
|
||||
pos.x = pos.y = 0
|
||||
f_copy.SetPosition(pos)
|
||||
if hasattr(f_copy, 'GetFootprintRect'):
|
||||
footprint_rect = f_copy.GetFootprintRect()
|
||||
else:
|
||||
footprint_rect = f_copy.GetBoundingBox(False, False)
|
||||
bbox = {
|
||||
"pos": self.normalize(f.GetPosition()),
|
||||
"relpos": self.normalize(footprint_rect.GetPosition()),
|
||||
"size": self.normalize(footprint_rect.GetSize()),
|
||||
"angle": self.normalize_angle(f.GetOrientation()),
|
||||
}
|
||||
|
||||
# graphical drawings
|
||||
drawings = []
|
||||
for d in f.GraphicalItems():
|
||||
# we only care about copper ones, silkscreen is taken care of
|
||||
if d.GetLayer() not in [pcbnew.F_Cu, pcbnew.B_Cu]:
|
||||
continue
|
||||
for drawing in self.parse_drawing(d):
|
||||
drawings.append({
|
||||
"layer": "F" if d.GetLayer() == pcbnew.F_Cu else "B",
|
||||
"drawing": drawing,
|
||||
})
|
||||
|
||||
# footprint pads
|
||||
pads = []
|
||||
for p in f.Pads():
|
||||
pad_dict = self.parse_pad(p)
|
||||
if pad_dict is not None:
|
||||
pads.append((p.GetPadName(), pad_dict))
|
||||
|
||||
if pads:
|
||||
# Try to guess first pin name.
|
||||
pads = sorted(pads, key=lambda el: el[0])
|
||||
pin1_pads = [p for p in pads if p[0] in
|
||||
['1', 'A', 'A1', 'P1', 'PAD1']]
|
||||
if pin1_pads:
|
||||
pin1_pad_name = pin1_pads[0][0]
|
||||
else:
|
||||
# No pads have common first pin name,
|
||||
# pick lexicographically smallest.
|
||||
pin1_pad_name = pads[0][0]
|
||||
for pad_name, pad_dict in pads:
|
||||
if pad_name == pin1_pad_name:
|
||||
pad_dict['pin1'] = 1
|
||||
|
||||
pads = [p[1] for p in pads]
|
||||
|
||||
# add footprint
|
||||
footprints.append({
|
||||
"ref": ref,
|
||||
"bbox": bbox,
|
||||
"pads": pads,
|
||||
"drawings": drawings,
|
||||
"layer": {
|
||||
pcbnew.F_Cu: "F",
|
||||
pcbnew.B_Cu: "B"
|
||||
}.get(f.GetLayer())
|
||||
})
|
||||
|
||||
return footprints
|
||||
|
||||
def parse_tracks(self, tracks):
|
||||
tent_vias = True
|
||||
if hasattr(self.board, "GetTentVias"):
|
||||
tent_vias = self.board.GetTentVias()
|
||||
result = {pcbnew.F_Cu: [], pcbnew.B_Cu: []}
|
||||
for track in tracks:
|
||||
if track.GetClass() in ["VIA", "PCB_VIA"]:
|
||||
track_dict = {
|
||||
"start": self.normalize(track.GetStart()),
|
||||
"end": self.normalize(track.GetEnd()),
|
||||
"width": track.GetWidth() * 1e-6,
|
||||
"net": track.GetNetname(),
|
||||
}
|
||||
if not tent_vias:
|
||||
track_dict["drillsize"] = track.GetDrillValue() * 1e-6
|
||||
for layer in [pcbnew.F_Cu, pcbnew.B_Cu]:
|
||||
if track.IsOnLayer(layer):
|
||||
result[layer].append(track_dict)
|
||||
else:
|
||||
if track.GetLayer() in [pcbnew.F_Cu, pcbnew.B_Cu]:
|
||||
if track.GetClass() in ["ARC", "PCB_ARC"]:
|
||||
a1, a2 = self.get_arc_angles(track)
|
||||
track_dict = {
|
||||
"center": self.normalize(track.GetCenter()),
|
||||
"startangle": a1,
|
||||
"endangle": a2,
|
||||
"radius": track.GetRadius() * 1e-6,
|
||||
"width": track.GetWidth() * 1e-6,
|
||||
}
|
||||
else:
|
||||
track_dict = {
|
||||
"start": self.normalize(track.GetStart()),
|
||||
"end": self.normalize(track.GetEnd()),
|
||||
"width": track.GetWidth() * 1e-6,
|
||||
}
|
||||
if self.config.include_nets:
|
||||
track_dict["net"] = track.GetNetname()
|
||||
result[track.GetLayer()].append(track_dict)
|
||||
|
||||
return {
|
||||
'F': result.get(pcbnew.F_Cu),
|
||||
'B': result.get(pcbnew.B_Cu)
|
||||
}
|
||||
|
||||
def parse_zones(self, zones):
|
||||
result = {pcbnew.F_Cu: [], pcbnew.B_Cu: []}
|
||||
for zone in zones: # type: pcbnew.ZONE
|
||||
if (not zone.IsFilled() or
|
||||
hasattr(zone, 'GetIsKeepout') and zone.GetIsKeepout() or
|
||||
hasattr(zone, 'GetIsRuleArea') and zone.GetIsRuleArea()):
|
||||
continue
|
||||
layers = [layer for layer in list(zone.GetLayerSet().Seq())
|
||||
if layer in [pcbnew.F_Cu, pcbnew.B_Cu]]
|
||||
for layer in layers:
|
||||
try:
|
||||
# kicad 5.1 and earlier
|
||||
poly_set = zone.GetFilledPolysList()
|
||||
except TypeError:
|
||||
poly_set = zone.GetFilledPolysList(layer)
|
||||
width = zone.GetMinThickness() * 1e-6
|
||||
if (hasattr(zone, 'GetFilledPolysUseThickness') and
|
||||
not zone.GetFilledPolysUseThickness()):
|
||||
width = 0
|
||||
zone_dict = {
|
||||
"polygons": self.parse_poly_set(poly_set),
|
||||
"width": width,
|
||||
}
|
||||
if self.config.include_nets:
|
||||
zone_dict["net"] = zone.GetNetname()
|
||||
result[layer].append(zone_dict)
|
||||
|
||||
return {
|
||||
'F': result.get(pcbnew.F_Cu),
|
||||
'B': result.get(pcbnew.B_Cu)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_netlist(net_info):
|
||||
# type: (pcbnew.NETINFO_LIST) -> list
|
||||
nets = net_info.NetsByName().asdict().keys()
|
||||
nets = sorted([str(s) for s in nets])
|
||||
return nets
|
||||
|
||||
@staticmethod
|
||||
def footprint_to_component(footprint, extra_fields):
|
||||
try:
|
||||
footprint_name = str(footprint.GetFPID().GetFootprintName())
|
||||
except AttributeError:
|
||||
footprint_name = str(footprint.GetFPID().GetLibItemName())
|
||||
|
||||
attr = 'Normal'
|
||||
if hasattr(pcbnew, 'FP_EXCLUDE_FROM_BOM'):
|
||||
if footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
|
||||
attr = 'Virtual'
|
||||
elif hasattr(pcbnew, 'MOD_VIRTUAL'):
|
||||
if footprint.GetAttributes() == pcbnew.MOD_VIRTUAL:
|
||||
attr = 'Virtual'
|
||||
layer = {
|
||||
pcbnew.F_Cu: 'F',
|
||||
pcbnew.B_Cu: 'B',
|
||||
}.get(footprint.GetLayer())
|
||||
|
||||
return Component(footprint.GetReference(),
|
||||
footprint.GetValue(),
|
||||
footprint_name,
|
||||
layer,
|
||||
attr,
|
||||
extra_fields)
|
||||
|
||||
def parse(self):
|
||||
from ..errors import ParsingException
|
||||
|
||||
# Get extra field data from netlist
|
||||
field_set = set(self.config.show_fields)
|
||||
field_set.discard("Value")
|
||||
field_set.discard("Footprint")
|
||||
need_extra_fields = (field_set or
|
||||
self.config.board_variant_whitelist or
|
||||
self.config.board_variant_blacklist or
|
||||
self.config.dnp_field)
|
||||
|
||||
if not self.config.extra_data_file and need_extra_fields:
|
||||
self.logger.warn('Ignoring extra fields related config parameters '
|
||||
'since no netlist/xml file was specified.')
|
||||
need_extra_fields = False
|
||||
|
||||
extra_field_data = None
|
||||
if (self.config.extra_data_file and
|
||||
os.path.isfile(self.config.extra_data_file)):
|
||||
extra_field_data = self.parse_extra_data(
|
||||
self.config.extra_data_file, self.config.normalize_field_case)
|
||||
|
||||
if extra_field_data is None and need_extra_fields:
|
||||
raise ParsingException(
|
||||
'Failed parsing %s' % self.config.extra_data_file)
|
||||
|
||||
title_block = self.board.GetTitleBlock()
|
||||
title = title_block.GetTitle()
|
||||
revision = title_block.GetRevision()
|
||||
company = title_block.GetCompany()
|
||||
file_date = title_block.GetDate()
|
||||
if (hasattr(self.board, "GetProject") and
|
||||
hasattr(pcbnew, "ExpandTextVars")):
|
||||
project = self.board.GetProject()
|
||||
title = pcbnew.ExpandTextVars(title, project)
|
||||
revision = pcbnew.ExpandTextVars(revision, project)
|
||||
company = pcbnew.ExpandTextVars(company, project)
|
||||
file_date = pcbnew.ExpandTextVars(file_date, project)
|
||||
|
||||
if not file_date:
|
||||
file_mtime = os.path.getmtime(self.file_name)
|
||||
file_date = datetime.fromtimestamp(file_mtime).strftime(
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
pcb_file_name = os.path.basename(self.file_name)
|
||||
if not title:
|
||||
# remove .kicad_pcb extension
|
||||
title = os.path.splitext(pcb_file_name)[0]
|
||||
edges, bbox = self.parse_edges(self.board)
|
||||
if bbox is None:
|
||||
self.logger.error('Please draw pcb outline on the edges '
|
||||
'layer on sheet or any footprint before '
|
||||
'generating BOM.')
|
||||
return None, None
|
||||
bbox = {
|
||||
"minx": bbox.GetPosition().x * 1e-6,
|
||||
"miny": bbox.GetPosition().y * 1e-6,
|
||||
"maxx": bbox.GetRight() * 1e-6,
|
||||
"maxy": bbox.GetBottom() * 1e-6,
|
||||
}
|
||||
|
||||
drawings = self.get_all_drawings()
|
||||
|
||||
pcbdata = {
|
||||
"edges_bbox": bbox,
|
||||
"edges": edges,
|
||||
"drawings": {
|
||||
"silkscreen": self.parse_drawings_on_layers(
|
||||
drawings, pcbnew.F_SilkS, pcbnew.B_SilkS),
|
||||
"fabrication": self.parse_drawings_on_layers(
|
||||
drawings, pcbnew.F_Fab, pcbnew.B_Fab),
|
||||
},
|
||||
"footprints": self.parse_footprints(),
|
||||
"metadata": {
|
||||
"title": title,
|
||||
"revision": revision,
|
||||
"company": company,
|
||||
"date": file_date,
|
||||
},
|
||||
"bom": {},
|
||||
"font_data": self.font_parser.get_parsed_font()
|
||||
}
|
||||
if self.config.include_tracks:
|
||||
pcbdata["tracks"] = self.parse_tracks(self.board.GetTracks())
|
||||
if hasattr(self.board, "Zones"):
|
||||
pcbdata["zones"] = self.parse_zones(self.board.Zones())
|
||||
else:
|
||||
self.logger.info("Zones not supported for KiCad 4, skipping")
|
||||
pcbdata["zones"] = {'F': [], 'B': []}
|
||||
if self.config.include_nets and hasattr(self.board, "GetNetInfo"):
|
||||
pcbdata["nets"] = self.parse_netlist(self.board.GetNetInfo())
|
||||
|
||||
if extra_field_data and need_extra_fields:
|
||||
extra_fields = extra_field_data.fields_by_index
|
||||
if extra_fields:
|
||||
extra_fields = extra_fields.values()
|
||||
|
||||
if extra_fields is None:
|
||||
extra_fields = []
|
||||
field_map = extra_field_data.fields_by_ref
|
||||
warning_shown = False
|
||||
|
||||
for f in self.footprints:
|
||||
extra_fields.append(field_map.get(f.GetReference(), {}))
|
||||
if f.GetReference() not in field_map:
|
||||
# Some components are on pcb but not in schematic data.
|
||||
# Show a warning about outdated extra data file.
|
||||
self.logger.warn(
|
||||
'Component %s is missing from schematic data.'
|
||||
% f.GetReference())
|
||||
warning_shown = True
|
||||
|
||||
if warning_shown:
|
||||
self.logger.warn('Netlist/xml file is likely out of date.')
|
||||
else:
|
||||
extra_fields = [{}] * len(self.footprints)
|
||||
|
||||
components = [self.footprint_to_component(f, e)
|
||||
for (f, e) in zip(self.footprints, extra_fields)]
|
||||
|
||||
return pcbdata, components
|
||||
|
||||
|
||||
class InteractiveHtmlBomPlugin(pcbnew.ActionPlugin, object):
|
||||
|
||||
def __init__(self):
|
||||
super(InteractiveHtmlBomPlugin, self).__init__()
|
||||
self.name = "Generate Interactive HTML BOM"
|
||||
self.category = "Read PCB"
|
||||
self.pcbnew_icon_support = hasattr(self, "show_toolbar_button")
|
||||
self.show_toolbar_button = True
|
||||
icon_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
self.icon_file_name = os.path.join(icon_dir, 'icon.png')
|
||||
self.description = "Generate interactive HTML page with BOM " \
|
||||
"table and pcb drawing."
|
||||
|
||||
def defaults(self):
|
||||
pass
|
||||
|
||||
def Run(self):
|
||||
from ..version import version
|
||||
from ..errors import ParsingException
|
||||
|
||||
logger = ibom.Logger()
|
||||
board = pcbnew.GetBoard()
|
||||
pcb_file_name = board.GetFileName()
|
||||
|
||||
if not pcb_file_name:
|
||||
logger.error('Please save the board file before generating BOM.')
|
||||
return
|
||||
|
||||
config = Config(version, os.path.dirname(pcb_file_name))
|
||||
parser = PcbnewParser(pcb_file_name, config, logger, board)
|
||||
|
||||
try:
|
||||
ibom.run_with_dialog(parser, config, logger)
|
||||
except ParsingException as e:
|
||||
logger.error(str(e))
|
Reference in New Issue
Block a user