From 68b2f9bd888f86766fea254d6253f3b1e88cc8a4 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sat, 14 Jul 2018 15:00:49 +0200 Subject: wip --- src/ee/fact/__init__.py | 49 +++++++ src/ee/fact/doit.py | 14 ++ src/ee/kicad/doit.py | 125 ++++++++++++++++ src/ee/kicad/export_gerber.py | 259 +++++++++++++++++++++++++++++++++ src/ee/tools/digikey_download_facts.py | 2 +- 5 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 src/ee/fact/__init__.py create mode 100644 src/ee/fact/doit.py create mode 100644 src/ee/kicad/doit.py create mode 100755 src/ee/kicad/export_gerber.py (limited to 'src') diff --git a/src/ee/fact/__init__.py b/src/ee/fact/__init__.py new file mode 100644 index 0000000..5c6d789 --- /dev/null +++ b/src/ee/fact/__init__.py @@ -0,0 +1,49 @@ +class Fact(object): + def __init__(self, kind): + self._kind = kind + self._domains = {} + + @property + def kind() -> str: + return self._kind + + def attr(self, domain: str, key: str): + try: + return self._domains[domain][key] + except KeyError: + pass + + def set(self, domain: str, key: str, value: str): + _kv(domain)[key] = value + + def _kv(self, domain: str): + try: + kv = self._domains[domain] + except KeyError: + kv = {} + self._domains[domain] = kv + + return kv + + def view(self, domain: str): + return FactKv(self, domain, self._kv(domain)) + + def write(self, output): + import configparser + ini = configparser.ConfigParser(interpolation = None) + for domain, kv in self._domains.items(): + ini.add_section(domain) + for key, value in kv.items(): + ini.set(domain, key, value) + + ini.write(output) + + +class FactKv(object): + def __init__(self, fact, domain, kv): + self._fact = fact + self._domain = domain + self._kv = kv + + def set(self, key, value): + self._kv[key] = value diff --git a/src/ee/fact/doit.py b/src/ee/fact/doit.py new file mode 100644 index 0000000..f922cf1 --- /dev/null +++ b/src/ee/fact/doit.py @@ -0,0 +1,14 @@ + + +def uptodate_if_isdir(path): + def do_check(): + return os.path.isdir(path) + return do_check + +def uptodate_if_older_than(dependency, *targets): + def do_check(): + dep_mtime = os.stat(dependency).st_mtime + mtimes = [os.stat(t).st_mtime for t in targets if os.path.isfile(t)] + target_mtime = max(mtimes) if mtimes else 0 + return dep_mtime < target_mtime + return do_check diff --git a/src/ee/kicad/doit.py b/src/ee/kicad/doit.py new file mode 100644 index 0000000..44abc61 --- /dev/null +++ b/src/ee/kicad/doit.py @@ -0,0 +1,125 @@ +import os.path +from doit.exceptions import TaskFailed +from doit.tools import check_timestamp_unchanged +from configclass import Config + +class KicadDoitTasks(object): + config = Config({ + "sch": None, + "kicad_pcb": None, + "gerber_dir": None, + "gerber_zip": None, + "components_dir": None, + }) + + def __init__(self, *args, **kwargs): + self.config = self.config.make(kwargs) + + def echo(*args, **kwargs): + print("_task: args={}, kwars={}".format(args, kwargs)) + + def tasks(self, *args, **kwargs): + import ee.kicad + kicad_pcb = self.config["kicad_pcb"] + sch = self.config["sch"] + tasks = [] + + gerber_dir = self.config["gerber_dir"] + if gerber_dir: + gerber_zip = self.config["gerber_zip"] or "{}.zip".format(gerber_dir) + #print("gerber_zip={}".format(gerber_zip)) + + eg = next((p for p in (os.path.join(p, "export_gerber.py") for p in ee.kicad.__path__) if os.path.isfile(p)), None) + if not eg: + raise Exception("Could not find export_gerber.py") + + # TODO: replace with python + mkdir = "mkdir -p {}".format(gerber_dir) + export_gerber = " ".join([ + eg, + "--pcb", kicad_pcb, + "--output-directory", gerber_dir, + "--protel-extensions", + ]) + def make_zip(): + import zipfile + from pathlib import Path + with zipfile.ZipFile(gerber_zip, "w") as z: + for p in Path(gerber_dir).iterdir(): + if not p.is_file(): + continue + z.write(p, arcname=p.relative_to(gerber_dir)) + + tasks.append({ + "name": "kicad_gerber", + "targets": [gerber_zip], + "actions": [mkdir, export_gerber, make_zip], + "file_dep": [kicad_pcb], + }) + + components_dir = self.config["components_dir"] + if sch and components_dir: + tasks.append(task_sch_to_component_files(sch, components_dir)) + + for t in tasks: + yield t + + +def task_sch_to_component_files(sch, components_dir): + def action(): + import ee.kicad + from ee.kicad.model import Component, ComponentField + import csv + import configparser + from ee.fact import Fact + + schematics = ee.kicad.read_schematics(sch) + cs = [c for c in schematics.components] + os.makedirs(components_dir, exist_ok=True) + # TODO: remove directory? + for c in cs: + path = os.path.join(components_dir, "{}.ini".format(c.timestamp)) + ini = configparser.ConfigParser(interpolation = None) + if os.path.isfile(path) and ini.read(path) != [path]: + print("Could not load component file: {}".format(path)) + return False +# if ini.has_section("kicad-schematic"): +# ini.remove_section("kicad-schematic") +# ini.add_section("kicad-schematic") +# ini.set("kicad-schematic", "ref", c.ref) +# ini.set("kicad-schematic", "ref-type", c.ref_type) + fact = Fact("schematic-symbol") + ks = fact.view("kicad-schematic") + ks.set("ref", c.ref) + ks.set("ref-type", c.ref_type) + if c.has_ref_num: +# ini.set("kicad-schematic", "ref-num", str(c.ref_num)) + ks.set("ref-num", str(c.ref_num)) +# ini.set("kicad-schematic", "value", c.value) + ks.set("value", c.value) + if c.footprint: +# ini.set("kicad-schematic", "footprint", c.footprint) + ks.set("footprint", c.footprint) + + fields = [f for f in c.fields if f.value and f.name not in ComponentField.names] + + if fields: +# if ini.has_section("kicad-fields"): +# ini.remove_section("kicad-fields") +# ini.add_section("kicad-fields") + kf = fact.kv("kicad-fields") + for f in fields: + kf.set(f.name, str(f.value)) + + with open(path, "w", newline="") as f: +# ini.write(f) + fact.write(f) + + component_files_cookie = "{}/components.cookie".format(components_dir) + gen_cookie = "date > %(targets)s" + return { + "name": "sch_to_component_files", + "file_dep": [sch], + "actions": [action, gen_cookie], + "targets": [component_files_cookie], + } diff --git a/src/ee/kicad/export_gerber.py b/src/ee/kicad/export_gerber.py new file mode 100755 index 0000000..511f9ee --- /dev/null +++ b/src/ee/kicad/export_gerber.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python +from __future__ import print_function +import sys +import os +import argparse +from pcbnew import * + +def layer_name_parser(s): + parts = s.split('=') + if len(parts) != 2: + raise argparse.ArgumentTypeError("Invalid layer renaming: " + s) + else: + return parts + +parser = argparse.ArgumentParser(description='KiCAD PCB to GERBER converter') + +parser.add_argument('--pcb', + required=True, + dest='pcb', + action='store', + help='A foo.kicad_pcb file') + +parser.add_argument('--output-directory', + required=True, + dest='output_directory', + action='store', + help='Directory to store output files') + +parser.add_argument('--detect-files-only', + dest='detect_files_only', + action='store', + help='Don\'t create the GERBER files, just write a list of to be created') + +parser.add_argument('--create-drill-map-file', + dest='create_drill_map_file', + action='store_true', + help='Create drill map file') + +parser.add_argument('--protel-extensions', + dest='protel_extensions', + action='store_true', + help='Use Protel filename extensions instead of .gbr') + +parser.add_argument('--extended-gerber-attributes', + dest='extended_gerber_attributes', + action='store_true', + help='Use extended Gerber attributes') + +parser.add_argument('--uppercase-extensions', + action='store_true', + help='Uppercase all extensions') + +parser.add_argument('--layer-extension', + dest='layer_extensions', + metavar='LAYER_AND_EXTENSION', + action='append', + type=layer_name_parser, + help='Set the file extension of a KiCAD layer. Format: =, example: F.SilkS=GSILK') + +args = parser.parse_args() +# print "args: " + str(args) +# print "args.name_layer: " + str(args.name_layers) + +renames = {} +if args.layer_extensions is not None: + for s in args.layer_extensions: + renames[s[0]] = s[1] +# print("renames: " + str(renames)) + +board = LoadBoard(args.pcb) + +class Plan: + def __init__(self, layerNum, layerName, description): + self.layerNum = layerNum + self.layerName = layerName + self.description = description + self.postfix = "" + self.ext = None + + @staticmethod + def standard(layerNum, description): + layerName = board.GetLayerName(layerNum) + return Plan(layerNum, layerName, description) + + @staticmethod + def copper(layerNum): + layerName = board.GetLayerName(layerNum) + description = "Copper Layer " + layerName + return Plan(layerNum, layerName, description) + +plot_plan = [Plan.standard(layerNum, description) for (layerNum, description) in [ + (F_SilkS, "Silk front"), + (F_Mask, "Mask front"), + (F_Paste, "Paste front"), + (B_SilkS, "Silk bottom"), + (B_Mask, "Mask bottom"), + (B_Paste, "Paste bottom"), + (Edge_Cuts, "Edges")]] + +layers = board.GetEnabledLayers() +for layerNum in layers.CuStack(): + plot_plan.append(Plan.copper(layerNum)) + +pctl = PLOT_CONTROLLER(board) +popt = pctl.GetPlotOptions() + +output_directory = args.output_directory +popt.SetOutputDirectory(output_directory) + +if not os.path.isdir(output_directory): + try: + os.makedirs(output_directory) + except: + print("Could not make output directory", file=sys.stderr) + sys.exit(1) + +# A nasty hack to get the base filename +pctl.SetLayer(F_Cu) +pctl.OpenPlotfile("", PLOT_FORMAT_GERBER, "") +filename = pctl.GetPlotFileName() +try: + os.remove(filename) +except: + pass +pctl.ClosePlot() + +# "Use protel filename extensions", default=False +popt.SetUseGerberProtelExtensions(args.protel_extensions) + +basename = os.path.splitext(filename)[0] + +drlFileOut = drlFile = basename + "-PTH.drl" +drlNpthFileOut = drlNpthFile = basename + "-NPTH.drl" + +if args.uppercase_extensions: + n, e = os.path.splitext(drlFileOut) + drlFileOut = n + e.upper() + n, e = os.path.splitext(drlNpthFileOut) + drlNpthFileOut = n + e.upper() + +values = vars(args) +for plan in plot_plan: + pctl.SetLayer(plan.layerNum) + pctl.OpenPlotfile("", PLOT_FORMAT_GERBER, plan.description) + filename = pctl.GetPlotFileName() + pctl.ClosePlot() + # By opening and closing the plot we create an empty plot file. + try: + os.remove(filename) + except: + pass + + filename, ext = os.path.splitext(filename) + if args.uppercase_extensions: + ext = ext.upper() + + if plan.layerName in renames: + ext = "." + renames[plan.layerName] + + # if plan.arg is not None: + # newExt = values[plan.arg] + # if newExt is not None: + # ext = "." + newExt + + plan.postfix = plan.layerName + plan.filename = basename + "-" + plan.postfix + ext + + # print "filename = " + plan.filename + ", postfix=" + plan.postfix + # print "filename: " + plan.filename + +if args.detect_files_only: + with open(args.detect_files_only, "w") as f: + for plan in plot_plan: + print(plan.filename, file=f) + + print(drlFileOut, file=f) + print(drlNpthFileOut, file=f) + sys.exit(0) + +# Set some important plot options: +popt.SetPlotFrameRef(False) + +# "Default line width (mm)", default=0.1mm +popt.SetLineWidth(FromMM(0.1)) + +popt.SetAutoScale(False) + +# "Mirrored plot", default=False. Not applicable for Gerber +popt.SetMirror(False) + +# "Include extended attributes", default=False +popt.SetUseGerberAttributes(args.extended_gerber_attributes) + +# "Drill marks" +popt.SetDrillMarksType(PCB_PLOT_PARAMS.NO_DRILL_SHAPE) + +# "Scaling" +popt.SetScale(1) + +# "Use auxilary axis as origin" +popt.SetUseAuxOrigin(False) + +# This by gerbers only (also the name is truly horrid!) +popt.SetSubtractMaskFromSilk(False) + +for plan in plot_plan: + pctl.SetLayer(plan.layerNum) + + # print "filename = " + plan.filename + ", postfix=" + plan.postfix + pctl.OpenPlotfile(plan.postfix, PLOT_FORMAT_GERBER, plan.description) + + actualFilename = pctl.GetPlotFileName() + expectedFilename = plan.filename + + if expectedFilename != actualFilename: + os.rename(actualFilename, expectedFilename) + + pctl.PlotLayer() + pctl.ClosePlot() + +drlwriter = EXCELLON_WRITER(board) +drlwriter.SetMapFileFormat(PLOT_FORMAT_GERBER) + +# "Drill file options" + +# "Mirror y axis", default=False +mirror = False + +# "Minimal header", default=True +minimalHeader = True + +offset = wxPoint(0, 0) + +# "Merge PTH and NPTH into one file", default=False +mergeNPTH = False +drlwriter.SetOptions(mirror, minimalHeader, offset, mergeNPTH) + +metricFmt = True +drlwriter.SetFormat(metricFmt) + +genDrl = True +genMap = args.create_drill_map_file + +drlwriter.CreateDrillandMapFilesSet(pctl.GetPlotDirName(), genDrl, genMap) + +# This has to be verified that this is the right way to do it. +if False: +# Check that the drill files actually was generated + if not os.path.isfile(drlFile): + print("No drill file generated: {}".format(drlFile), file=sys.stderr) + + if not os.path.isfile(drlNpthFile): + print("No drill file generated: {}".format(drlNpthFile), file=sys.stderr) + +if drlFile != drlFileOut: + os.rename(drlFile, drlFileOut) + +if drlNpthFile != drlNpthFileOut: + os.rename(drlNpthFile, drlNpthFileOut) diff --git a/src/ee/tools/digikey_download_facts.py b/src/ee/tools/digikey_download_facts.py index 923b996..54ff640 100644 --- a/src/ee/tools/digikey_download_facts.py +++ b/src/ee/tools/digikey_download_facts.py @@ -46,7 +46,7 @@ parser.add_argument("--force", args = parser.parse_args() digikey = dk.Digikey() -client = dk.DigikeyClient(digikey, on_download=log.debug) +client = dk.DigikeyClient(digikey, event_handler=lambda kind, msg: (log.debug if kind == "on-download" else log.info)(msg)) repo = dk.DigikeyRepository(digikey, args.out) -- cgit v1.2.3