diff --git a/docs/fabrication/intro.md b/docs/fabrication/intro.md index b3db77c6..cc9df33a 100644 --- a/docs/fabrication/intro.md +++ b/docs/fabrication/intro.md @@ -35,7 +35,7 @@ Note: click on the name of the manufacturer to see corresponding documentation: - [JLC PCB](jlcpcb.md): board manufacturing, SMD assembly. [https://jlcpcb.com/](https://jlcpcb.com/) - [PCBWay](pcbway.md): board manufacturing, assembly. [https://www.pcbway.com/](https://www.pcbway.com/) - [OSH Park](oshpark.md): board manufacturing. [https://oshpark.com/](https://oshpark.com/) - +- [Neoden YY1](neodenyy1.md): desktop PCB assembly. [https://neodenusa.com/neoden-yy1-pick-place-machine](https://neodenusa.com/neoden-yy1-pick-place-machine) ## Adding New Fabrication Houses diff --git a/docs/fabrication/neodenyy1.md b/docs/fabrication/neodenyy1.md new file mode 100644 index 00000000..3550a2f1 --- /dev/null +++ b/docs/fabrication/neodenyy1.md @@ -0,0 +1,46 @@ +# Fabrication: Neoden YY1 + +The basic usage of this exporter is: +``` +kikit fab neodenyy1 [OPTIONS] BOARD OUTPUTDIR +``` + +When you run this command, you will find file `top_pos.csv` and `bottom_pos.csv` +in `OUTPUTDIR`. This file can be used in Neoden YY1. KiKit automatically detects +the number of layers. + +If you want to name your files differently, you can specify `--nametemplate`. +This option takes a string that should contain `{}`. This string will be +replaced by `gerber`, `pos` or `bom` in the out file names. The extension is +appended automatically. + +## Assembly + +For Neoden YY1 you must specify `--assembly` option and provide the board +`--schematic `. KiKit will generate files: `top_pos.csv` +(top layer component placement) and `bottom_pos.csv` (bottom layer component +placement). Use these two files to assembly PCB on machine. + +On Neoden YY1, the position origin must use the bottom left corner of the board +edge. + +## Correction of the Footprint Position + +It is possible that orientation footprints in your SMD does not match the +orientation of the components in the SMD assembly service. There are two +solutions: + +- correct the orientation in the library or +- apply KiKit's orientation corrections. + +The first option is not always feasible - e.g., when you use KiCAD's built-in +libraries or you are preparing a board for multiple fabrication houses and each +of them uses a different orientation. + +KiKit allows you to specify the origin and orientation correction of the +position. The correction is specified by `YY1_CORRECTION` field. The field +value is a semicolon separated tuple: `; ; ` with values in +millimeters and degrees. You can read the XY corrections by hovering cursor over +the intended origin in footprint editor and mark the coordinates. Note that +first the rotation correction is applied, then the translation. Usually, you +will need only the rotation correction. diff --git a/kikit/fab/common.py b/kikit/fab/common.py index 44dbbf96..de6145a5 100644 --- a/kikit/fab/common.py +++ b/kikit/fab/common.py @@ -148,6 +148,9 @@ def applyCorrectionPattern(correctionPatterns, footprint): return (corpat.x_correction, corpat.y_correction, corpat.rotation) return (0, 0, 0) +def noFilter(footprint): + return True + def collectPosData(board, correctionFields, posFilter=lambda x : True, footprintX=defaultFootprintX, footprintY=defaultFootprintY, bom=None, correctionFile=None): diff --git a/kikit/fab/jlcpcb.py b/kikit/fab/jlcpcb.py index 430d27af..bc9a1a3b 100644 --- a/kikit/fab/jlcpcb.py +++ b/kikit/fab/jlcpcb.py @@ -55,9 +55,6 @@ def bomToCsv(bomData, filename): value, footprint, lcsc = cType writer.writerow([value, ",".join(refChunk), footprint, lcsc]) -def noFilter(footprint): - return True - def exportJlcpcb(board, outputdir, assembly, schematic, ignore, field, corrections, correctionpatterns, missingerror, nametemplate, drc): """ diff --git a/kikit/fab/neodenyy1.py b/kikit/fab/neodenyy1.py new file mode 100644 index 00000000..85536f32 --- /dev/null +++ b/kikit/fab/neodenyy1.py @@ -0,0 +1,160 @@ +import click +from pcbnewTransition import pcbnew +import csv +import os +import sys +import shutil +import re +from pathlib import Path +from kikit.fab.common import * +from kikit.common import * +from kikit.units import mm + +FOOTPRIINTREGEX = { + re.compile(r'Capacitor_SMD:C_(\d+)_.*'): 'C_{}', + re.compile(r'Diode_SMD:D_(\d+)_.*'): 'D_{}', + re.compile(r'Inductor_SMD:L_(\d+)_.*'): 'L_{}', + re.compile(r'Resistor_SMD:R_(\d+)_.*'): 'R_{}', + re.compile(r'Crystal:Crystal_SMD_(.*?)_.*'): 'CRYSTAL_{}' +} + +def collectBom(components, ignore): + bom = {} + for c in components: + if getUnit(c) != 1: + continue + reference = getReference(c) + if reference.startswith("#PWR") or reference.startswith("#FL"): + continue + if reference in ignore: + continue + if hasattr(c, "in_bom") and not c.in_bom: + continue + if hasattr(c, "on_board") and not c.on_board: + continue + if hasattr(c, "dnp") and c.dnp: + continue + cType = ( + getField(c, "Value"), + getField(c, "Footprint") + ) + bom[cType] = bom.get(cType, []) + [reference] + return bom + +def transcodeFootprint(footprint): + for pattern, replacement in FOOTPRIINTREGEX.items(): + matchedFootprint = pattern.match(footprint) + if matchedFootprint != None: + return replacement.format(matchedFootprint.groups()[0]) + matchedFootprint = footprint.split(':') + if len(matchedFootprint) > 1: + return matchedFootprint[1].split('_')[0] + else: + return footprint + +def posDataProcess(posData, pcbSize, bom): + topLayer = [] + bottomLayer = [] + ref = {} + for cType, references in bom.items(): + sortedReferences = sorted(references, key=naturalComponentKey) + for refComponent in sortedReferences: + ref[refComponent] = cType + for line in posData: + if line[0] in ref: + value, footprint = ref[line[0]] + if line[3] == 'T': + topLayer.append(line + (value, footprint, line[1], line[2],)) + elif line[3] == 'B': + # Neoden YY1 need the position on the bottom layer need position origin from bottom right corner, but KiCad only support one origin on all layer, so calculate it by using PCB BoundingBox Width + bottomLayer.append(line + (value, footprint, pcbSize[1] - line[1], line[2], )) + else: + value = None + footprint = None + if line[3] == 'T': + topLayer.append(line + (value, footprint, line[1], line[2],)) + elif line[3] == 'B': + # Neoden YY1 need the position on the bottom layer need position origin from bottom right corner, but KiCad only support one origin on all layer, so calculate it by using PCB BoundingBox Width + bottomLayer.append(line + (value, footprint, pcbSize[1] - line[1], line[2],)) + return (topLayer, bottomLayer) + +""" + Export pos file for Neoden YY1 + Neoden YY1 file is in csv format. +""" +def posDataToCSV(layerData, prepend, filename): + basename = os.path.basename(filename) + basename = prepend + '_' + basename + dirname = os.path.dirname(filename) + with open(os.path.join(dirname, basename), "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile) + # First line is fixed with `NEODEN,YY1,P&P FILE,,,,,,,,,,,` + writer.writerow(["NEODEN","YY1","P&P FILE","","","","","","","","","","",""]) + writer.writerow(["","","","","","","","","","","","","",""]) + # This line is for Panelized, make it deafult to not panelized, if anyone need panelized assembly, just change it on the machine. + writer.writerow(["PanelizedPCB","UnitLength","0","UnitWidth","0","Rows","1","Columns","1",""]) + writer.writerow(["","","","","","","","","","","","","",""]) + # Neoden YY1 only support one Fiducial on the board, make Fiducial as 0 to disable Fiducial correction method, if anyone need it just set it on the machine. + # OverallOffset is the global offset. This depends on the real task, just ignore it and set it on the machine when you need. + writer.writerow(["Fiducial","1-X","0","1-Y","0","OverallOffsetX","0.00","OverallOffsetY","0.00",""]) + writer.writerow(["","","","","","","","","","","","","",""]) + # Automatic Nozzle Changer, Neoden YY1 only support 4 Nozzle Change task in one project. Nozzle Setting and Nozzle Station Setting is different for every user, so disable it by default, edit it by user when needed. + # ["NozzleChange","(Enable Nozzle change task? ON/OFF)","BeforeComponent","1","Head1","Drop","Station2","PickUp","Station1",""] + writer.writerow(["NozzleChange","OFF","BeforeComponent","1","Head1","Drop","Station2","PickUp","Station1",""]) + writer.writerow(["NozzleChange","OFF","BeforeComponent","2","Head2","Drop","Station3","PickUp","Station2",""]) + writer.writerow(["NozzleChange","OFF","BeforeComponent","1","Head1","Drop","Station1","PickUp","Station1",""]) + writer.writerow(["NozzleChange","OFF","BeforeComponent","1","Head1","Drop","Station1","PickUp","Station1",""]) + writer.writerow(["","","","","","","","","","","","","",""]) + # Neoden YY1 using Comment and Footprint for batch feeder selection when in edit mode. Neoden YY1 only support 2 decimal digits. + # "Head" is for Picker, it has two picker, 0 for all picker, 1 for picker 1, 2 for picker 2. + # "FeederNo" to define which feeder should be use, every user have different feeder setting, so just make it to use feeder 1 and left for user to edit. + # "Mode" is how to confirm the component is picked, 0 - disable, 1 - camera, 2 - vacuum, 3 - camera and vacuum, 4 - camera for big IC + # "Skip" should this line skipped by machine? + writer.writerow(["Designator","Comment","Footprint","Mid X(mm)","Mid Y(mm)","Rotation","Head","FeederNo","Mount Speed(%)","Pick Height(mm)","Place Height(mm)","Mode","Skip"]) + for line in sorted(layerData, key=lambda x: naturalComponentKey(x[0])): + line = list(line) + skip = "0" + if line[5] == None or line[6] == None: + skip = "1" + line[5] = "Unknown" + line[6] = "Unknown" + line = [line[0], line[5], transcodeFootprint(line[6]), line[7], line[8], line[4], "0", "1", "100", "0", "0", "1", skip] + for i in [3, 4, 5]: + line[i] = f"{line[i]:.2f}" # Most Fab houses expect only 2 decimal digits + writer.writerow(line) + +def posDataToFile(posData, pcbSize, bom, filename): + topLayer, bottomLayer = posDataProcess(posData=posData,pcbSize = pcbSize, bom=bom) + posDataToCSV(topLayer, 'top', filename) + posDataToCSV(bottomLayer, 'bottom', filename) + +def exportNeodenYY1(board, outputdir, schematic, ignore, + corrections, correctionpatterns, nametemplate, drc): + if schematic is None: + raise RuntimeError("When outputing assembly data, schematic is required") + + ensureValidBoard(board) + loadedBoard = pcbnew.LoadBoard(board) + + if drc: + ensurePassingDrc(loadedBoard) + + refsToIgnore = parseReferences(ignore) + removeComponents(loadedBoard, refsToIgnore) + Path(outputdir).mkdir(parents=True, exist_ok=True) + + ensureValidSch(schematic) + + correctionFields = [x.strip() for x in corrections.split(",")] + components = extractComponents(schematic) + bom = collectBom(components, refsToIgnore) + + posData = collectPosData(loadedBoard, correctionFields, + bom=components, posFilter=noFilter, correctionFile=correctionpatterns) + boardReferences = set([x[0] for x in posData]) + bom = {key: [v for v in val if v in boardReferences] for key, val in bom.items()} + bom = {key: val for key, val in bom.items() if len(val) > 0} + + boundingBox = loadedBoard.GetBoardEdgesBoundingBox() + pcbSize = (boundingBox.GetHeight() / mm, boundingBox.GetWidth() / mm, ) + posDataToFile(posData, pcbSize, bom, os.path.join(outputdir, expandNameTemplate(nametemplate, "pos", loadedBoard) + ".csv")) diff --git a/kikit/fab_ui.py b/kikit/fab_ui.py index dfed7d07..cd0e566a 100644 --- a/kikit/fab_ui.py +++ b/kikit/fab_ui.py @@ -98,6 +98,22 @@ def oshpark(**kwargs): app = fakeKiCADGui() return execute(oshpark.exportOSHPark, kwargs) +@click.command() +@fabCommand +@click.option("--schematic", type=click.Path(dir_okay=False), help="Board schematics (required for assembly files)") +@click.option("--ignore", type=str, default="", help="Comma separated list of designators to exclude from SMT assembly") +@click.option("--corrections", type=str, default="YY1_CORRECTION", + help="Comma separated list of component fields with the correction value. First existing field is used") +@click.option("--correctionpatterns", type=click.Path(dir_okay=False)) +def neodenyy1(**kwargs): + """ + Prepare fabrication files for Neoden YY1 + """ + from kikit.fab import neodenyy1 + from kikit.common import fakeKiCADGui + app = fakeKiCADGui() + return execute(neodenyy1.exportNeodenYY1, kwargs) + @click.group() def fab(): """ @@ -108,3 +124,4 @@ def fab(): fab.add_command(jlcpcb) fab.add_command(pcbway) fab.add_command(oshpark) +fab.add_command(neodenyy1)