2021-02-14 08:51:14 -08:00
|
|
|
#!/usr/bin/env python
|
|
|
|
############################################################################################
|
|
|
|
# Hellen-One: A board rendering script.
|
2021-02-21 09:56:08 -08:00
|
|
|
#
|
|
|
|
# here we produde andreika-git-iBom using gerbers, placement and BOM files as input.
|
|
|
|
# andreika-git-iBom adds 3D view of the PCB into the original https://github.com/openscopeproject/InteractiveHtmlBom
|
|
|
|
#
|
2021-02-14 08:51:14 -08:00
|
|
|
# (c) andreika <prometheus.pcb@gmail.com>
|
|
|
|
############################################################################################
|
2021-02-21 09:54:39 -08:00
|
|
|
#
|
|
|
|
# based on iBom-template.html produced by https://github.com/openscopeproject/InteractiveHtmlBom
|
|
|
|
#
|
|
|
|
|
2021-02-14 08:51:14 -08:00
|
|
|
|
|
|
|
import os, sys
|
|
|
|
import re
|
|
|
|
import json, csv
|
|
|
|
import math, copy
|
|
|
|
import base64
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
sys.path.append("./bin/InteractiveHtmlBom/InteractiveHtmlBom/core")
|
|
|
|
from lzstring import LZString
|
|
|
|
|
|
|
|
if len(sys.argv) < 12:
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("Error! Please specify all the parameters!")
|
2021-02-14 08:51:14 -08:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
boardName = sys.argv[1]
|
|
|
|
revision = sys.argv[2]
|
|
|
|
renderedPcbDpi = sys.argv[3]
|
|
|
|
|
|
|
|
keepoutPath = sys.argv[4]
|
|
|
|
topSilkPath = sys.argv[5]
|
|
|
|
renderedPcbPath = sys.argv[6]
|
|
|
|
bomPath = sys.argv[7]
|
|
|
|
cplPath = sys.argv[8]
|
|
|
|
footprintsPath = sys.argv[9]
|
|
|
|
fixRotationsPath = sys.argv[10]
|
|
|
|
iBomFilePath = sys.argv[11]
|
|
|
|
|
|
|
|
htmlFileName = './bin/iBom-template.html'
|
|
|
|
inch_to_mm = 25.4
|
|
|
|
|
|
|
|
|
|
|
|
def getFormat(xI, xD, yI, yD, yInvert):
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("* Format: ", xI, xD, yI, yD)
|
2021-02-14 08:51:14 -08:00
|
|
|
fmt_pat_x = re.compile(r'^([0-9]{'+xI+'})([0-9]{'+xD+'})$')
|
|
|
|
fmt_pat_y = re.compile(r'^([0-9]{'+yI+'})([0-9]{'+yD+'})$')
|
|
|
|
return [fmt_pat_x, fmt_pat_y, yInvert]
|
|
|
|
|
|
|
|
def getCoords(pos, format):
|
|
|
|
x = format[0].match(pos[0])
|
|
|
|
y = format[1].match(pos[1])
|
|
|
|
if x and y:
|
|
|
|
x = float(x.group(1) + "." + x.group(2)) * inch_to_mm
|
|
|
|
y = float(y.group(1) + "." + y.group(2)) * inch_to_mm
|
|
|
|
if format[2]:
|
|
|
|
y = format[2] - y
|
|
|
|
return [x, y]
|
|
|
|
sys.exit('Error! Cannot parse position ' + str(pos))
|
|
|
|
|
|
|
|
def getSize(size, format):
|
|
|
|
if type(size) is list:
|
|
|
|
size = 0.5 * (float(size[0]) + float(size[1]))
|
|
|
|
return float(size) * inch_to_mm
|
|
|
|
|
|
|
|
def readGerber(filePath, yInvert):
|
|
|
|
json = []
|
|
|
|
apertList = dict()
|
|
|
|
apert_circle_pat = re.compile(r'^%ADD([0-9]+)C,([+\-0-9\.]+)(X([+\-0-9\.]+))?\*%$')
|
|
|
|
apert_rect_pat = re.compile(r'^%ADD([0-9]+)R,([+\-0-9\.]+)(X([+\-0-9\.]+))?(X([+\-0-9\.]+))?\*%$')
|
|
|
|
op_pat = re.compile(r'^(X([+\-0-9]+))?(Y([+\-0-9]+))?D([0-9]+)\*$')
|
|
|
|
format_pat = re.compile(r'^%FSLAX([0-9])([0-9])Y([0-9])([0-9])\*%$')
|
|
|
|
cur_x = "0"
|
|
|
|
cur_y = "0"
|
|
|
|
cur_size = "0"
|
|
|
|
cur_aper_type = ""
|
|
|
|
inPoly = False
|
|
|
|
polyStartPos = []
|
|
|
|
polyOutlines = []
|
|
|
|
polyPoints = []
|
|
|
|
minCoord = [ 99999, 99999 ]
|
|
|
|
maxCoord = [ -99999, -99999 ]
|
|
|
|
format = getFormat("2", "5", "2", "5", yInvert)
|
|
|
|
invertedMask = False
|
2022-03-26 12:30:24 -07:00
|
|
|
with open(filePath, 'rt') as f:
|
2021-02-14 08:51:14 -08:00
|
|
|
for line in f:
|
|
|
|
line = line.strip()
|
|
|
|
#print line
|
|
|
|
if line == "%LPC*%":
|
|
|
|
invertedMask = True
|
|
|
|
if line == "%LPD*%":
|
|
|
|
invertedMask = False
|
|
|
|
fmt = format_pat.match(line)
|
|
|
|
if fmt:
|
|
|
|
format = getFormat(fmt.group(1), fmt.group(2), fmt.group(3), fmt.group(4), yInvert)
|
|
|
|
apertCircle = apert_circle_pat.match(line)
|
|
|
|
if apertCircle:
|
|
|
|
apertNum = int(apertCircle.group(1))
|
|
|
|
apertSize = apertCircle.group(2)
|
|
|
|
apertList[apertNum] = ["circle", apertSize]
|
2022-03-26 12:30:24 -07:00
|
|
|
# print ("* Aperture C: " + str(apertNum) + " = " + apertSize)
|
2021-02-14 08:51:14 -08:00
|
|
|
|
|
|
|
apertRect = apert_rect_pat.match(line)
|
|
|
|
if apertRect:
|
|
|
|
apertNum = int(apertRect.group(1))
|
|
|
|
apertSizeX = apertRect.group(2)
|
|
|
|
apertSizeY = apertRect.group(4)
|
|
|
|
apertList[apertNum] = ["rect", [apertSizeX, apertSizeY]]
|
2022-03-26 12:30:24 -07:00
|
|
|
# print ("* Aperture R: " + str(apertNum) + " = " + apertSizeX + " " + apertSizeY)
|
2021-02-14 08:51:14 -08:00
|
|
|
|
|
|
|
op = op_pat.match(line)
|
|
|
|
if op:
|
|
|
|
x = op.group(2) or cur_x
|
|
|
|
y = op.group(4) or cur_y
|
|
|
|
op = op.group(5)
|
|
|
|
# draw
|
|
|
|
if op == "01":
|
|
|
|
curCoords = getCoords([x, y], format)
|
|
|
|
minCoord[0] = min(curCoords[0], minCoord[0])
|
|
|
|
minCoord[1] = min(curCoords[1], minCoord[1])
|
|
|
|
maxCoord[0] = max(curCoords[0], maxCoord[0])
|
|
|
|
maxCoord[1] = max(curCoords[1], maxCoord[1])
|
|
|
|
|
|
|
|
if inPoly:
|
|
|
|
polyPoints.append(curCoords)
|
|
|
|
else:
|
|
|
|
jsonLine = {"type": "segment", "width": getSize(cur_size, format), "start": getCoords([cur_x, cur_y], format), "end": curCoords}
|
|
|
|
json.append(jsonLine)
|
|
|
|
# move
|
|
|
|
if op == "02":
|
|
|
|
if inPoly and len(polyPoints) > 0:
|
|
|
|
polyOutlines.append(polyPoints)
|
|
|
|
polyPoints = []
|
|
|
|
# flash
|
|
|
|
if op == "03":
|
|
|
|
curCoords = getCoords([x, y], format)
|
|
|
|
if cur_aper_type == "circle":
|
|
|
|
# todo: approximate with poly?
|
|
|
|
jsonCircle = {"type": "segment", "width": 0.2, "start": curCoords, "radius": getSize(cur_size, format) }
|
|
|
|
json.append(jsonCircle)
|
|
|
|
if cur_aper_type == "rect":
|
|
|
|
xS = getSize(cur_size[0], format) / 2
|
|
|
|
yS = getSize(cur_size[1], format) / 2
|
|
|
|
color = "#fff" if invertedMask else "#aa4"
|
|
|
|
jsonPoly = {"type": "polygon", "pos": curCoords, "angle": 0, "color": color, "polygons": [[[-xS, -yS], [xS, -yS], [xS, yS], [-xS, yS]]]}
|
|
|
|
json.append(jsonPoly)
|
|
|
|
if int(op) > 3:
|
|
|
|
a = int(op)
|
|
|
|
cur_aper_type = apertList[a][0]
|
|
|
|
cur_size = apertList[a][1]
|
2022-03-26 12:30:24 -07:00
|
|
|
# print ("* Changing aperture: ", a)
|
2021-02-14 08:51:14 -08:00
|
|
|
else:
|
|
|
|
cur_x = x
|
|
|
|
cur_y = y
|
|
|
|
if line == "G36*": # region begin
|
|
|
|
polyStartPos = [ cur_x, cur_y ]
|
|
|
|
polyOutlines = []
|
|
|
|
polyPoints = []
|
|
|
|
inPoly = True
|
|
|
|
if line == "G37*": # region end
|
|
|
|
color = "#fff" if invertedMask else "#aa4"
|
|
|
|
jsonPoly = {"type": "polygon", "pos": [0, 0], "angle": 0, "color": color, "polygons": []}
|
|
|
|
for ol in polyOutlines:
|
|
|
|
jsonOutline = []
|
|
|
|
for point in ol:
|
|
|
|
jsonOutline.append(point)
|
|
|
|
jsonPoly["polygons"].append(jsonOutline)
|
|
|
|
json.append(jsonPoly)
|
|
|
|
inPoly = False
|
|
|
|
f.close()
|
|
|
|
return { "json": json, "min": minCoord, "max": maxCoord }
|
|
|
|
|
|
|
|
def updateBbox(bbox, padXY, padWH):
|
|
|
|
bbox[0][0] = min(bbox[0][0], padXY[0] - padWH[0])
|
|
|
|
bbox[0][1] = min(bbox[0][1], padXY[1] - padWH[1])
|
|
|
|
bbox[1][0] = max(bbox[1][0], padXY[0] + padWH[0])
|
|
|
|
bbox[1][1] = max(bbox[1][1], padXY[1] + padWH[1])
|
|
|
|
return bbox
|
|
|
|
|
|
|
|
def readFootprint(fpname, footprintsPath, des):
|
|
|
|
if not fpname:
|
|
|
|
return None
|
2022-01-15 00:40:51 -08:00
|
|
|
pat_module = re.compile(r'\((module|footprint)\s+\"?([\w\-\.\:]+)\"?\s+\(layer\s+\"?([FB])')
|
|
|
|
pat_pad = re.compile(r'^\s*\(pad\s+\"?([0-9A-Z]+)\"?\s+(\w+)\s+(\w+)\s+\(at\s+([+\-0-9e\.]+)\s+([+\-0-9e\.]+)\s*([+\-0-9\.]+)?\)\s+\(size\s+([+\-0-9\.]+)\s+([+\-0-9\.]+)\)(\s*\(drill\s+([+\-0-9\.]+)\))?\s+\(layer[s]?\s+\"?([^\)]+)\)(\s*\(roundrect_rratio\s+([+\-0-9\.]+)\))?')
|
2021-02-14 08:51:14 -08:00
|
|
|
|
|
|
|
fpFileName = footprintsPath + "/" + fpname + ".kicad_mod"
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("* Reading " + fpFileName)
|
2021-02-14 08:51:14 -08:00
|
|
|
if not os.path.isfile(fpFileName):
|
|
|
|
print("* Error! Footprint NOT FOUND! Skipping " + des)
|
|
|
|
return None
|
|
|
|
|
|
|
|
json = {"drawings": [], "pads": []}
|
|
|
|
bbox = [[ 9999.0, 9999.0 ], [ -9999.0, -9999.0 ]]
|
|
|
|
with open(fpFileName, 'rt') as f:
|
|
|
|
for line in f:
|
|
|
|
module = pat_module.match(line)
|
|
|
|
if module:
|
2022-01-15 00:40:51 -08:00
|
|
|
json["layer"] = module.group(3)
|
2021-02-14 08:51:14 -08:00
|
|
|
pad = pat_pad.match(line)
|
|
|
|
if pad:
|
|
|
|
padIdx = pad.group(1)
|
|
|
|
padType = pad.group(2)
|
|
|
|
padShape = pad.group(3)
|
|
|
|
padX = pad.group(4)
|
|
|
|
padY = pad.group(5)
|
|
|
|
padRot = pad.group(6) if pad.group(6) else "0"
|
|
|
|
padW = pad.group(7)
|
|
|
|
padH = pad.group(8)
|
|
|
|
padDrill = pad.group(10) if pad.group(10) else "0"
|
|
|
|
padLayers = pad.group(11)
|
|
|
|
padRrect = pad.group(13) if pad.group(13) else "0"
|
|
|
|
bbox = updateBbox(bbox, [float(padX), float(padY)], [float(padW) * 0.5, float(padH) * 0.5])
|
|
|
|
pad = {
|
|
|
|
"layers": ["F"], # todo: parse layers
|
|
|
|
"type": ("smd" if padType == "smd" else "th"),
|
|
|
|
"pos": [ float(padX), float(padY) ],
|
|
|
|
"size": [ float(padW), float(padH) ],
|
|
|
|
#"offset": [ 0.0, 0.0 ],
|
|
|
|
"angle": float(padRot),
|
|
|
|
"shape": padShape
|
|
|
|
}
|
|
|
|
if padIdx == "1":
|
|
|
|
pad["pin1"] = 1
|
|
|
|
if padShape == "roundrect":
|
|
|
|
pad["radius"] = padRrect
|
|
|
|
if padType != "smd":
|
|
|
|
pad["drillsize"] = [float(padDrill), float(padDrill)]
|
|
|
|
json["pads"].append(pad)
|
|
|
|
json["bbox"] = {
|
|
|
|
"relpos": bbox[0],
|
|
|
|
"size": [bbox[1][0] - bbox[0][0], bbox[1][1] - bbox[0][1]],
|
|
|
|
}
|
|
|
|
return json
|
|
|
|
|
|
|
|
def getPosValue(c):
|
|
|
|
return float(c.replace("mm", ""))
|
|
|
|
|
|
|
|
def rotate(origin, point, angle):
|
2022-01-14 23:10:00 -08:00
|
|
|
# the angle is inverted because the Y-axis is inverted
|
|
|
|
angleRad = math.radians(-angle)
|
2021-02-14 08:51:14 -08:00
|
|
|
[ox, oy] = origin
|
|
|
|
[px, py] = point
|
|
|
|
|
|
|
|
qx = ox + math.cos(angleRad) * (px - ox) - math.sin(angleRad) * (py - oy)
|
|
|
|
qy = oy + math.sin(angleRad) * (px - ox) + math.cos(angleRad) * (py - oy)
|
|
|
|
return [qx, qy]
|
|
|
|
|
|
|
|
def readFootprints(bomPath, cplPath, footprintsPath, yInvert):
|
|
|
|
json = {"footprints": [],
|
|
|
|
"bom": {
|
|
|
|
"both": [],
|
|
|
|
"F": [],
|
|
|
|
"B": [],
|
|
|
|
"skipped": []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bom = {}
|
|
|
|
bomlut = []
|
|
|
|
footprints = {}
|
|
|
|
rotations = {}
|
|
|
|
# read rotations csv (to undo weird JLC's angles which are not footprint-oriented)
|
2022-03-26 12:30:24 -07:00
|
|
|
with open(fixRotationsPath, 'rt') as f:
|
2021-02-14 08:51:14 -08:00
|
|
|
next(f)
|
|
|
|
reader = csv.reader(f, delimiter=',')
|
|
|
|
for row in reader:
|
|
|
|
rotations[row[0]] = float(row[1])
|
|
|
|
# read BOM csv
|
2022-03-26 12:30:24 -07:00
|
|
|
with open(bomPath, 'rt') as f:
|
2021-02-14 08:51:14 -08:00
|
|
|
next(f)
|
|
|
|
reader = csv.reader(f, delimiter=',')
|
|
|
|
for row in reader:
|
|
|
|
if not row[2]:
|
|
|
|
print("* Skipping an empty footprint for (" + row[1] + ")...")
|
|
|
|
continue
|
|
|
|
bb = row[1].split(", ")
|
2021-05-19 22:48:40 -07:00
|
|
|
bomlut.append({ "value": row[0], "fp": row[2], "refs": [] })
|
2021-02-14 08:51:14 -08:00
|
|
|
idx = len(bomlut) - 1
|
|
|
|
for b in bb:
|
|
|
|
bom[b] = { "fp": row[2], "idx": idx }
|
|
|
|
# read CPL csv
|
2022-03-26 12:30:24 -07:00
|
|
|
with open(cplPath, 'rt') as f:
|
2021-02-14 08:51:14 -08:00
|
|
|
reader = csv.reader(f, delimiter=',')
|
|
|
|
for row in reader:
|
|
|
|
if row[0] in bom:
|
|
|
|
fpname = bom[row[0]]["fp"]
|
|
|
|
idx = bom[row[0]]["idx"]
|
|
|
|
# search the stored footprint or load a new one
|
|
|
|
if fpname in footprints:
|
|
|
|
fprint = footprints[fpname]
|
|
|
|
else:
|
|
|
|
fprint = readFootprint(fpname, footprintsPath, row[0])
|
|
|
|
# if the footprint is not found, we cannot add it to the iBOM
|
|
|
|
if not fprint:
|
2021-05-19 12:39:42 -07:00
|
|
|
if idx in bomlut:
|
|
|
|
bomlut.remove(idx)
|
|
|
|
del bom[row[0]]
|
2021-02-14 08:51:14 -08:00
|
|
|
continue
|
|
|
|
footprints[fpname] = fprint
|
|
|
|
fpr = copy.deepcopy(fprint)
|
|
|
|
fpr["ref"] = row[0]
|
|
|
|
fpr["bbox"]["pos"] = [ getPosValue(row[1]), getPosValue(row[2]) ]
|
|
|
|
fpr["bbox"]["angle"] = float(row[4])
|
|
|
|
origin = [0, 0]
|
|
|
|
rotation = 0
|
|
|
|
# reverse the JLC's rotation if this is one of 'special' corrected footprints
|
|
|
|
for rot in rotations:
|
|
|
|
if re.match(rot, fpname):
|
|
|
|
rotation = -rotations[rot]
|
|
|
|
fpr["bbox"]["angle"] += rotation
|
2022-01-14 23:10:00 -08:00
|
|
|
fpr["bbox"]["pos"][1] = yInvert - fpr["bbox"]["pos"][1]
|
2021-02-14 08:51:14 -08:00
|
|
|
for p in range(len(fpr["pads"])):
|
|
|
|
# move and rotate the pads according to the CPL data
|
|
|
|
fpr["pads"][p]["pos"] = rotate(origin, fpr["pads"][p]["pos"], fpr["bbox"]["angle"])
|
|
|
|
fpr["pads"][p]["pos"][0] += fpr["bbox"]["pos"][0]
|
|
|
|
fpr["pads"][p]["pos"][1] += fpr["bbox"]["pos"][1]
|
|
|
|
fpr["pads"][p]["angle"] += fpr["bbox"]["angle"]
|
|
|
|
json["footprints"].append(fpr)
|
|
|
|
fid = len(json["footprints"]) - 1
|
|
|
|
bomlut[idx]["refs"].append([row[0], fid])
|
|
|
|
|
|
|
|
for b in bomlut:
|
|
|
|
refs = b["refs"]
|
2021-05-19 12:39:42 -07:00
|
|
|
if len(refs) < 1:
|
|
|
|
print ("* Skipping DNP component: " + b["value"])
|
|
|
|
continue
|
2021-02-14 08:51:14 -08:00
|
|
|
bomItem = [
|
|
|
|
len(refs),
|
|
|
|
b["value"],
|
2021-05-19 22:48:40 -07:00
|
|
|
b["fp"],
|
2021-02-14 08:51:14 -08:00
|
|
|
refs,
|
|
|
|
[]
|
|
|
|
]
|
|
|
|
# todo: handle layers
|
|
|
|
json["bom"]["F"].append(bomItem)
|
|
|
|
json["bom"]["both"].append(bomItem)
|
|
|
|
return json
|
|
|
|
|
|
|
|
###################################################################
|
|
|
|
|
2022-03-26 12:30:24 -07:00
|
|
|
with open(htmlFileName, 'rt') as f:
|
2021-02-14 08:51:14 -08:00
|
|
|
html = f.read()
|
|
|
|
f.close()
|
|
|
|
|
|
|
|
data = {
|
|
|
|
"footprints": [ ],
|
|
|
|
"edges": [],
|
|
|
|
"ibom_version": "v2.3",
|
|
|
|
"bom": { },
|
|
|
|
"silkscreen": { "B": [], "F": [] },
|
|
|
|
"edges_bbox": { },
|
|
|
|
"font_data": { },
|
|
|
|
"metadata": { "date": datetime.now().strftime("%Y-%m-%d, %H:%M:%S"), "company": "rusEFI", "revision": revision, "title": "Hellen One: " + boardName }
|
|
|
|
}
|
|
|
|
|
|
|
|
# first, get yInvert
|
|
|
|
keepout = readGerber(keepoutPath, None)
|
|
|
|
|
|
|
|
yInvert = keepout["min"][1] + keepout["max"][1]
|
|
|
|
keepout = readGerber(keepoutPath, yInvert)
|
|
|
|
|
|
|
|
#print json.dumps(edges)
|
|
|
|
data["edges"] = keepout["json"]
|
|
|
|
data["edges_bbox"]["minx"] = keepout["min"][0]
|
|
|
|
data["edges_bbox"]["miny"] = keepout["min"][1]
|
|
|
|
data["edges_bbox"]["maxx"] = keepout["max"][0]
|
|
|
|
data["edges_bbox"]["maxy"] = keepout["max"][1]
|
|
|
|
|
|
|
|
topSilk = readGerber(topSilkPath, yInvert)
|
|
|
|
data["silkscreen"]["F"] = topSilk["json"]
|
|
|
|
|
|
|
|
footprints = readFootprints(bomPath, cplPath, footprintsPath, yInvert)
|
|
|
|
data["footprints"] = footprints["footprints"]
|
|
|
|
data["bom"] = footprints["bom"]
|
|
|
|
|
|
|
|
print("* Compressing the data...")
|
|
|
|
jsonText = json.dumps(data)
|
|
|
|
|
|
|
|
print("* Adding the pcb image...")
|
|
|
|
with open(renderedPcbPath, mode='rb') as f:
|
|
|
|
renderedPcb = f.read()
|
|
|
|
html = html.replace('___PCBDPI___', renderedPcbDpi)
|
2022-03-26 12:30:24 -07:00
|
|
|
html = html.replace('___PCBIMAGE___', 'data:image/png;base64,' + base64.b64encode(renderedPcb).decode('ascii'))
|
2021-02-14 08:51:14 -08:00
|
|
|
|
|
|
|
print("* Adding the BOM data...")
|
|
|
|
jsonBase64 = LZString().compress_to_base64(jsonText)
|
|
|
|
html = html.replace('___PCBDATA___', jsonBase64)
|
|
|
|
|
|
|
|
print("* Writing the output BOM file...")
|
|
|
|
|
|
|
|
with open(iBomFilePath, "wt") as wf:
|
|
|
|
wf.write(html)
|
|
|
|
wf.close()
|
|
|
|
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("Done!")
|
2021-02-14 08:51:14 -08:00
|
|
|
|