diff --git a/kidiff_gui.py b/kidiff_gui.py
index 27c356f..846ea91 100755
--- a/kidiff_gui.py
+++ b/kidiff_gui.py
@@ -658,6 +658,36 @@ a:hover {
# ----------------------Main Functions begin here---------------------------------------
#
+
+def getGitPath(prjctName, prjctPath):
+ gitRootCmd = 'cd ' + prjctPath + ' && ' + gitProg + ' rev-parse --show-toplevel'
+
+ gitRootProcess = Popen(
+ gitRootCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = gitRootProcess.communicate()
+
+ gitRoot = stdout.decode('utf-8')
+
+ gitPathCmd = 'cd ' + _escape_string(gitRoot) + ' && ' + gitProg + ' ls-tree -r --name-only HEAD | ' + grepProg + ' -m 1 ' + prjctName
+
+ gitPathProcess = Popen(
+ gitPathCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = gitPathProcess.communicate()
+
+ gitPathProcess.wait()
+
+ return stdout.decode('utf-8')
+
def getGitDiff(diff1, diff2, prjctName, prjctPath):
'''Given two git artifacts, write out two kicad_pcb files to their respective
directories (named after the artifact). Returns the date and time of both commits'''
@@ -694,11 +724,13 @@ def getGitDiff(diff1, diff2, prjctName, prjctPath):
if not os.path.exists(outputDir2):
os.makedirs(outputDir2)
+ gitPath = getGitPath(prjctName, prjctPath)
+
gitArtifact1 = 'cd ' + prjctPath + ' && ' + gitProg + ' show ' + artifact1 + \
- ':' + prjctName + ' > ' + outputDir1 + '/' + prjctName
+ ':' + gitPath + ' > ' + outputDir1 + '/' + prjctName
gitArtifact2 = 'cd ' + prjctPath + ' && ' + gitProg + ' show ' + artifact2 + \
- ':' + prjctName + ' > ' + outputDir2 + '/' + prjctName
+ ':' + gitPath + ' > ' + outputDir2 + '/' + prjctName
print(gitArtifact1, gitArtifact2)
diff --git a/kidiff_linux.py b/kidiff_linux.py
new file mode 100755
index 0000000..7eeada5
--- /dev/null
+++ b/kidiff_linux.py
@@ -0,0 +1,1603 @@
+#!/usr/bin/env python3
+#
+# A python script to select two revisions of a Kicad pcbnew layout
+# held in a suitable version control repository and produce a graphical diff
+# of generated svg files in a web browser.
+
+# TODO [ ] Place all template text/css text in external files.
+# TODO [ ] Improve display of artifacts in diff choice window.
+# TODO [ ] Consider changing GUI elements to wxPython.
+# TODO [*] Manage any missing SCM paths - reflect available SCM progs in splash screen.
+# TODO [ ] Adjust
for three pane output to have white outer border & pan-zoom control, not filter colour.
+# TODO [ ] Improve three pane output layout, perhaps with diff tree on LHS and not underneath.
+
+# DEBUG Tk window not being destroyed when closed.
+# DEBUG Minor error with parsing FP_Text diff.
+
+import os
+import time
+import subprocess
+import tkinter as tk
+import webbrowser
+from subprocess import PIPE, STDOUT, Popen
+from tkinter import *
+from tkinter import filedialog, ttk
+from tkinter.messagebox import showinfo
+import tkUI
+from tkUI import *
+import http.server
+import socketserver
+socketserver.TCPServer.allow_reuse_address = True
+
+if sys.version_info[0] >= 3:
+ unicode = str
+
+def _escape_string( val ):
+ # Make unicode
+ val = unicode( val )
+ # Escape stuff
+ val = val.replace( u'\\', u'\\\\' )
+ val = val.replace( u' ', u'\\ ' )
+ return ''.join(val.splitlines())
+
+# -------------------------------------------------------------------------
+# NOTE Adjust these paths to suit your setup
+# If you do not use one (or more) of these SCMs, please set to ''
+# This program attempts to auto-identify which SCM is in use.
+# In the event of multiple SCMs being in use in one repository, the order of priority
+# is Fossil > Git > SVN.
+
+gitProg = '/usr/bin/git'
+fossilProg = ''
+svnProg = '/usr/bin/svn'
+plotDir = '/plots'
+webDir = '/web'
+diffProg = '/usr/bin/diff'
+plotProg = '/home/spendless/sandpit/KiCad-Diff/plotPCB.py'
+# plotProg = '/usr/local/bin/plotPCB.py'
+# plotProg = '/usr/local/bin/plotPCB_macOS.py'
+grepProg = '/usr/bin/grep'
+
+
+# -------------------------------------------------------------------------
+# NOTE Adjust this port to suit your requirements. Must be >1000.
+
+PORT = 9092
+
+
+# -------------------------------------------------------------------------
+# NOTE Please adjust these colours to suit your requirements.
+
+layerCols = {
+ 'F_Cu': "#952927",
+ 'B_Cu': "#359632",
+ 'B_Paste': "#3DC9C9",
+ 'F_Paste': "#969696",
+ 'F_SilkS': "#339697",
+ 'B_SilkS': "#481649",
+ 'B_Mask': "#943197",
+ 'F_Mask': "#943197",
+ 'Edge_Cuts': "#C9C83B",
+ 'Margin': "#D357D2",
+ 'In1_Cu': "#C2C200",
+ 'In2_Cu': "#C200C2",
+ 'Dwgs_User': "#0364D3",
+ 'Cmts_User': "#7AC0F4",
+ 'Eco1_User': "#008500",
+ 'Eco2_User': "#C2C200",
+ 'B_Fab': "#858585",
+ 'F_Fab': "#C2C200",
+ 'B_Adhes': "#3545A8",
+ 'F_Adhes': "#A74AA8",
+ 'B_CrtYd': "#D3D04B",
+ 'F_CrtYd': "#A7A7A7",
+}
+
+Handler = http.server.SimpleHTTPRequestHandler
+
+
+# ------------------------------------------HTML Template Blocks-------------------------------------------
+#
+# FIXME These should go into external files to clean up and seperate the code
+
+
+tail = '''
+
+
+'''
+
+indexHead = '''
+
+
+
+
+
+
+
+
+
+
+
+ Title: {TITLE}
+ Company: {COMPANY}
+
+
+ Thickness (mm)
+
+
+ {THICK1}
+
+
+ {THICK2}
+
+
+
+ Modules
+
+
+ {MODULES1}
+
+
+ {MODULES2}
+
+
+
+ Drawings
+
+
+ {DRAWINGS1}
+
+
+ {DRAWINGS2}
+
+
+
+
+ Version
+
+
+ {diffDir1}
+
+
+ {diffDir2}
+
+
+ Nets
+
+
+ {NETS1}
+
+
+ {NETS2}
+
+
+
+
+ Date
+
+
+ {D1DATE}
+
+
+ {D2DATE}
+
+
+ Tracks
+
+
+ {TRACKS1}
+
+
+ {TRACKS2}
+
+
+
+
+ Time
+
+
+ {D1TIME}
+
+
+ {D2TIME}
+
+
+ Zones
+
+
+ {ZONES1}
+
+
+ {ZONES2}
+
+
+
+
+
+'''
+
+outfile = '''
+
+
+
+
+
+
{layer}
+
+
+'''
+
+tryptychHTML = '''
+
+
+
+
+
+
+
+
+
+
{prj}
+
{layer}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+twopane='''
+
+
+
+
+
+'''
+
+css = '''
+body {
+ background-color: #2c3031;
+ margin: 0 auto;
+ max-width: 45cm;
+ border: 1pt solid #586e75;
+ padding: 0.5em;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ border-color: #e2e3e3;
+ width: 100%;
+ height: 2px;
+ border: 2px
+}
+
+html {
+ background-color: #222222;
+ color: #e2e3e3;
+ margin: 1em;
+}
+
+.tabbed {
+ float: left;
+ width: 100%;
+ padding: 0 6px;
+}
+
+.tabbed>input {
+ display: none;
+}
+
+.tabbed>section>h1 {
+ font: 14px arial, sans-serif;
+ float: left;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0.5em 0.1em 0;
+ overflow: hidden;
+ font-size: 1em;
+ font-weight: normal;
+}
+
+.tabbed>input:first-child+section>h1 {
+ padding-left: 1em;
+}
+
+.tabbed>section>h1>label {
+ font: 14px arial, sans-serif;
+ display: block;
+ padding: 0.25em 0.75em;
+ border: 1px solid #ddd;
+ border-bottom: none;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.0625);
+ background: rgb(50, 50, 50);
+ cursor: pointer;
+}
+
+.tabbed>section>div {
+ position: relative;
+ z-index: 1;
+ float: right;
+ box-sizing: border-box;
+ width: 100%;
+ margin: 1.95em 0 0 -100%;
+ padding: 0.25em 0.75em;
+ border: 1px solid #ddd;
+
+ box-shadow: 0 0 1em rgb(245, 245, 245);
+ background: rgba(70, 67, 67, 0.185);
+}
+
+.tabbed>input:checked+section>h1 {
+ position: relative;
+ z-index: 2;
+ border-bottom: none;
+}
+
+
+.tabbed>input:not(:checked)+section>div {
+ display: none;
+}
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+
+.gallery {
+ border: 1px solid #ccc;
+ background-color: #222;
+ padding: 5px;
+ align: middle;
+ vertical-align: middle;
+}
+
+.gallery:hover {
+ border: 1px solid #777;
+}
+
+.gallery img {
+ width: 100%;
+ height: auto;
+}
+
+.desc,
+.title {
+ padding: 10px;
+ text-align: center;
+ font: 12px arial, sans-serif;
+}
+
+.title,
+.subtitle,
+.details {
+ padding-left: 10px;
+ text-align: left;
+ font: 20px arial, sans-serif;
+ color: #dddddd;
+}
+
+.subtitle {
+ font: 14px arial, sans-serif;
+}
+
+.details,
+.versions {
+ padding: 5px;
+ font: 12px arial, sans-serif;
+ padding-bottom: 5px;
+}
+
+
+.differences {
+ font: 12px courier, monospace;
+ padding: 5px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+.responsive {
+ padding: 0 6px;
+ float: left;
+ width: 19.99999%;
+ margin: 6px 0;
+}
+
+@media only screen and (max-width:700px) {
+ .responsive {
+ width: 49.98%;
+ margin: 6px 0;
+ }
+}
+
+@media only screen and (max-width:500px) {
+ .responsive {
+ width: 100%;
+ margin: 6px 0;
+ }
+}
+
+.responsivefull {
+ padding: 0 6px;
+ width: 100%;
+ margin: 3px 0;
+}
+
+.clearfix:after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.box {
+ float: left;
+ width: 20px;
+ height: 20px;
+ margin: 5px;
+ border: 1px solid rgba(0, 0, 0, .2);
+}
+
+.red {
+ background: #832320;
+}
+
+.green {
+ background: #44808aa8;
+}
+
+
+.added {
+ color: #5eb6c4;
+ text-align: left;
+}
+
+.removed {
+ color: #ba312d;
+ text-align: right;
+}
+
+.tbr td {
+ color:#ba312d;
+ padding: 10px;
+ font: 12px arial,sans-serif;
+ padding-bottom: 5px;
+}
+
+.tbl td {
+ color: #5eb6c4;
+ padding: 10px;
+ font: 12px arial, sans-serif;
+ padding-bottom: 5px;
+}
+
+.tbr th {
+ text-align: left;
+ background: #832320;
+ padding: 10px;
+ font: 12px arial, sans-serif;
+ font-weight: bold;
+ padding-bottom: 5px;
+}
+
+.tbl th {
+ text-align: left;
+ background: #44808aa8;
+ padding: 10px;
+ font: 12px arial, sans-serif;
+ font-weight: bold;
+ padding-bottom: 5px;
+}
+
+.F_Cu {
+ filter: invert(28%) sepia(50%) saturate(2065%) hue-rotate(334deg) brightness(73%) contrast(97%);
+}
+
+.B_Cu {
+ filter: invert(44%) sepia(14%) saturate(2359%) hue-rotate(70deg) brightness(103%) contrast(82%);
+}
+
+.B_Paste {
+ filter: invert(91%) sepia(47%) saturate(4033%) hue-rotate(139deg) brightness(82%) contrast(91%);
+}
+
+.F_Paste {
+ filter: invert(57%) sepia(60%) saturate(6%) hue-rotate(314deg) brightness(92%) contrast(99%);
+}
+
+.F_SilkS {
+ filter: invert(46%) sepia(44%) saturate(587%) hue-rotate(132deg) brightness(101%) contrast(85%);
+}
+
+.B_SilkS {
+ filter: invert(14%) sepia(27%) saturate(2741%) hue-rotate(264deg) brightness(95%) contrast(102%);
+}
+
+.B_Mask {
+ filter: invert(22%) sepia(56%) saturate(2652%) hue-rotate(277deg) brightness(94%) contrast(87%);
+}
+
+.F_Mask {
+ filter: invert(27%) sepia(51%) saturate(1920%) hue-rotate(269deg) brightness(89%) contrast(96%);
+}
+
+.Edge_Cuts {
+ filter: invert(79%) sepia(79%) saturate(401%) hue-rotate(6deg) brightness(88%) contrast(88%);
+}
+
+.Margin {
+ filter: invert(74%) sepia(71%) saturate(5700%) hue-rotate(268deg) brightness(89%) contrast(84%);
+}
+
+.In1_Cu {
+ filter: invert(69%) sepia(39%) saturate(1246%) hue-rotate(17deg) brightness(97%) contrast(104%);
+}
+
+.In2_Cu {
+ filter: invert(14%) sepia(79%) saturate(5231%) hue-rotate(293deg) brightness(91%) contrast(119%);
+}
+
+.Dwgs_User {
+ filter: invert(40%) sepia(68%) saturate(7431%) hue-rotate(203deg) brightness(89%) contrast(98%);
+}
+
+.Cmts_User {
+ filter: invert(73%) sepia(10%) saturate(1901%) hue-rotate(171deg) brightness(95%) contrast(102%);
+}
+
+.Eco1_User {
+ filter: invert(25%) sepia(98%) saturate(2882%) hue-rotate(109deg) brightness(90%) contrast(104%);
+}
+
+.Eco2_User {
+ filter: invert(85%) sepia(21%) saturate(5099%) hue-rotate(12deg) brightness(91%) contrast(102%);
+}
+
+.B_Fab {
+ filter: invert(60%) sepia(0%) saturate(0%) hue-rotate(253deg) brightness(87%) contrast(90%);
+}
+
+.F_Fab {
+ filter: invert(71%) sepia(21%) saturate(4662%) hue-rotate(21deg) brightness(103%) contrast(100%);
+}
+
+.B_Adhes {
+ filter: invert(24%) sepia(48%) saturate(2586%) hue-rotate(218deg) brightness(88%) contrast(92%);
+}
+
+.F_Adhes {
+ filter: invert(38%) sepia(49%) saturate(1009%) hue-rotate(254deg) brightness(88%) contrast(86%);
+}
+
+.B_CrtYd {
+ filter: invert(79%) sepia(92%) saturate(322%) hue-rotate(3deg) brightness(89%) contrast(92%);
+}
+
+.F_CrtYd {
+ filter: invert(73%) sepia(1%) saturate(0%) hue-rotate(116deg) brightness(92%) contrast(91%);
+}
+'''
+
+# ----------------------Main Functions begin here---------------------------------------
+#
+
+def getGitPath(prjctName, prjctPath):
+ gitRootCmd = 'cd ' + prjctPath + ' && ' + gitProg + ' rev-parse --show-toplevel'
+
+ gitRootProcess = Popen(
+ gitRootCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = gitRootProcess.communicate()
+
+ gitRoot = stdout.decode('utf-8')
+
+ gitPathCmd = 'cd ' + _escape_string(gitRoot) + ' && ' + gitProg + ' ls-tree -r --name-only HEAD | ' + grepProg + ' -m 1 ' + prjctName
+
+ gitPathProcess = Popen(
+ gitPathCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = gitPathProcess.communicate()
+
+ gitPathProcess.wait()
+
+ return _escape_string(stdout.decode('utf-8'))
+
+def getGitDiff(diff1, diff2, prjctName, prjctPath):
+ '''Given two git artifacts, write out two kicad_pcb files to their respective
+ directories (named after the artifact). Returns the date and time of both commits'''
+
+ artifact1 = diff1[:6]
+ artifact2 = diff2[:6]
+
+ findDiff = 'cd ' + _escape_string(prjctPath) + ' && ' + gitProg + ' diff --name-only ' + \
+ artifact1 + ' ' + artifact2 + ' | ' + grepProg + ' .kicad_pcb'
+
+ changes = Popen(
+ findDiff,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = changes.communicate()
+ changes.wait()
+ changed = (stdout.decode('utf-8'))
+
+ if changed == '':
+ print("No .kicad_pcb files differ between these commits")
+ sys.exit()
+
+ outputDir1 = prjctPath + plotDir + '/' + artifact1
+ outputDir2 = prjctPath + plotDir + '/' + artifact2
+
+ if not os.path.exists(outputDir1):
+ os.makedirs(outputDir1)
+
+ if not os.path.exists(outputDir2):
+ os.makedirs(outputDir2)
+
+ gitPath = getGitPath(prjctName, _escape_string(prjctPath))
+
+ gitArtifact1 = 'cd ' + _escape_string(prjctPath) + ' && ' + gitProg + ' show ' + artifact1 + \
+ ':' + gitPath + ' > ' + _escape_string(outputDir1) + '/' + prjctName
+
+ gitArtifact2 = 'cd ' + _escape_string(prjctPath) + ' && ' + gitProg + ' show ' + artifact2 + \
+ ':' + gitPath + ' > ' + _escape_string(outputDir2) + '/' + prjctName
+
+ ver1 = Popen(
+ gitArtifact1,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = ver1.communicate()
+
+ ver2 = Popen(
+ gitArtifact2,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = ver2.communicate()
+
+ ver1.wait(); ver2.wait()
+
+ gitDateTime1 = 'cd ' + _escape_string(prjctPath) + ' && ' + gitProg + ' show -s --format="%ci" ' + artifact1
+ gitDateTime2 = 'cd ' + _escape_string(prjctPath) + ' && ' + gitProg + ' show -s --format="%ci" ' + artifact2
+
+ print(gitDateTime1,gitDateTime2)
+
+ dt1 = Popen(
+ gitDateTime1,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = dt1.communicate()
+ dt1.wait()
+
+
+ dateTime1 = stdout.decode('utf-8')
+ date1, time1, UTC = dateTime1.split(' ')
+
+ dt2 = Popen(
+ gitDateTime2,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = dt2.communicate()
+ dt2.wait()
+
+ dateTime2 = stdout.decode('utf-8')
+ date2, time2, UTC = dateTime2.split(' ')
+
+ times = date1 + " " + time1 + " " + date2 + " " + time2
+ print(times)
+ return (times)
+
+
+def getSVNDiff(diff1, diff2, prjctName, prjctPath):
+ '''Given two SVN revisions, write out two kicad_pcb files to their respective
+ directories (named after the revision number). Returns the date and time of both commits'''
+
+ svnChanged = 'cd ' + prjctPath + ' && svn diff --summarize -r ' + \
+ diff1 + ':' + diff2 + ' ' + prjctName
+
+ changed = Popen(
+ svnChanged,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = changed.communicate()
+ changed.wait()
+
+ changed, *boardName = (stdout.decode('utf-8'))
+
+ if changed != 'M':
+ print("No .kicad_pcb files differ between these commits")
+ sys.exit()
+
+ outputDir1 = prjctPath + plotDir + '/' + diff1
+ outputDir2 = prjctPath + plotDir + '/' + diff2
+
+ if not os.path.exists(outputDir1):
+ os.makedirs(outputDir1)
+
+ if not os.path.exists(outputDir2):
+ os.makedirs(outputDir2)
+
+ SVNdiffCmd1 = 'cd ' + prjctPath + ' && svn cat -r ' + diff1 + \
+ " " + prjctName + ' > ' + outputDir1 + '/' + prjctName
+ SVNdiffCmd2 = 'cd ' + prjctPath + ' && svn cat -r ' + diff2 + \
+ " " + prjctName + ' > ' + outputDir2 + '/' + prjctName
+
+ ver1 = Popen(
+ SVNdiffCmd1,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = ver1.communicate()
+
+ ver2 = Popen(
+ SVNdiffCmd2,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = ver2.communicate()
+
+ ver1.wait(); ver2.wait()
+
+ dateTime1 = 'cd ' + prjctPath + ' && svn log -r' + diff1
+ dateTime2 = 'cd ' + prjctPath + ' && svn log -r' + diff2
+
+ dt1 = Popen(
+ dateTime1,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = dt1.communicate()
+
+ dt1.wait()
+ dateTime = stdout.decode('utf-8')
+ cmt = (dateTime.splitlines()[1]).split('|')
+ _, SVNdate1, SVNtime1, SVNutc, *_ = cmt[2].split(' ')
+
+ dt2 = Popen(
+ dateTime2,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = dt2.communicate()
+ dt2.wait()
+ dateTime = stdout.decode('utf-8')
+ cmt = (dateTime.splitlines()[1]).split('|')
+ _, SVNdate2, SVNtime2, SVNutc, *_ = cmt[2].split(' ')
+
+ times = SVNdate1 + " " + SVNtime1 + " " + SVNdate2 + " " + SVNtime2
+
+ print(times)
+
+ return (times)
+
+
+def getFossilDiff(diff1, diff2, prjctName, prjctPath):
+ '''Given two Fossil artifacts, write out two kicad_pcb files to their respective
+ directories (named after the artifacts). Returns the date and time of both commits'''
+
+ artifact1 = diff1[:6]
+ artifact2 = diff2[:6]
+
+ findDiff = 'cd ' + _escape_string(prjctPath) + ' && fossil diff --brief -r ' + \
+ artifact1 + ' --to ' + artifact2 + ' | ' + grepProg + ' .kicad_pcb'
+
+ changes = Popen(
+ findDiff,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = changes.communicate()
+ changes.wait()
+
+ changed = (stdout.decode('utf-8'))
+ print(changed)
+ if changed == '':
+ print("No .kicad_pcb files differ between these commits")
+ sys.exit()
+
+ outputDir1 = prjctPath + plotDir + '/' + artifact1
+ outputDir2 = prjctPath + plotDir + '/' + artifact2
+
+ if not os.path.exists(outputDir1):
+ os.makedirs(outputDir1)
+
+ if not os.path.exists(outputDir2):
+ os.makedirs(outputDir2)
+
+ fossilArtifact1 = 'cd ' + _escape_string(prjctPath) + ' && fossil cat ' + _escape_string(prjctPath) + '/' + prjctName + \
+ ' -r ' + artifact1 + ' > ' + outputDir1 + '/' + prjctName
+ fossilArtifact2 = 'cd ' + _escape_string(prjctPath) + ' && fossil cat ' + _escape_string(prjctPath) + '/' + prjctName + \
+ ' -r ' + artifact2 + ' > ' + outputDir2 + '/' + prjctName
+
+ fossilInfo1 = 'cd ' + _escape_string(prjctPath) + ' && fossil info ' + artifact1
+ fossilInfo2 = 'cd ' + _escape_string(prjctPath) + ' && fossil info ' + artifact2
+
+ ver1 = Popen(
+ fossilArtifact1,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = ver1.communicate()
+ ver1.wait()
+
+ info1 = Popen(
+ fossilInfo1,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = info1.communicate()
+ info1.wait()
+
+ dateTime = stdout.decode('utf-8')
+ dateTime = dateTime.strip()
+ uuid, _, _, _, _, _, _, _, _, artifactRef, dateDiff1, timeDiff1, *junk1 = dateTime.split(
+ " ")
+
+ ver2 = Popen(
+ fossilArtifact2,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = ver2.communicate()
+ ver2.wait()
+
+ info2 = Popen(
+ fossilInfo2,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = info2.communicate()
+ info2.wait()
+
+ dateTime = stdout.decode('utf-8')
+ dateTime = dateTime.strip()
+ uuid, _, _, _, _, _, _, _, _, artifactRef, dateDiff2, timeDiff2, *junk1 = dateTime.split(
+ " ")
+
+ dateTime = dateDiff1 + " " + timeDiff1 + " " + dateDiff2 + " " + timeDiff2
+
+ print(dateTime)
+
+ return dateTime
+
+
+def getProject():
+ '''File select dialogue. Opens Tk File browser and
+ selector set for .kicad_pcb files. Returns path and file name
+ '''
+ selected = tk.filedialog.askopenfile(
+ initialdir="~/",
+ title="Select kicad_pcb file in a VC directory",
+ filetypes=(("KiCad pcb files", "*.kicad_pcb"), ("all files", "*.*")))
+ if selected:
+ path, prjct = os.path.split(selected.name)
+
+ return (path, prjct)
+
+
+def getSCM(prjctPath):
+ '''Determines which SCM methodology is in place when passed the enclosing
+ directory. NB there is no facility to deal with directories with multiple VCS in place
+ and current order of priority is Git > Fossil > SVN.
+ Easy to add additional SCMs but also would need to write handling code
+ '''
+
+ scm = ''
+
+ # check if SVN program installed and then check if *.kicad_pcb is in a SVN checkout
+ if (svnProg != ''):
+ svnCmd = 'cd ' + prjctPath + ' && ' + svnProg + ' log | perl -l4svn log0pe "s/^-+/\n/"'
+ svn = Popen(
+ svnCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = svn.communicate()
+ svn.wait()
+ if ((stdout.decode('utf-8') != '') & (stderr.decode('utf-8') == '')):
+ scm = 'SVN'
+
+ # check if Fossil program installed and then check if *.kicad_pcb is in a Fossil checkout
+ if (fossilProg != ''):
+ fossilCmd = 'cd ' + prjctPath + ' && ' + fossilProg + ' status'
+ fossil = Popen(
+ fossilCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = fossil.communicate()
+ fossil.wait()
+ # print(stdout.decode('utf-8'),"stdERROR=", stderr.decode('utf-8'))
+ if (stdout.decode('utf-8') != ''):
+ scm = 'Fossil'
+
+ # Check if Git program installed and then check if *.kicad_pcb is in a Git checkout
+ if (gitProg != ''):
+ gitCmd = 'cd ' + prjctPath + ' && ' + gitProg + ' status'
+ git = Popen(
+ gitCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = git.communicate()
+ git.wait()
+ if ((stdout.decode('utf-8') != '') & (stderr.decode('utf-8') == '')):
+ scm = 'Git'
+
+ return scm
+
+
+def fossilDiff(path, kicadPCB):
+ '''Returns list of Fossil artifacts from a directory containing a
+ *.kicad_pcb file.'''
+
+ # NOTE Assemble a list of artefacts. Unfortunatly, Fossil doesn't give any easily configurable length.
+ # NOTE Using the -W option results in multiline diffs
+ # NOTE 'fossil -finfo' looks like this
+ # 2017-05-19 [21d331ea6b] Preliminary work on CvPCB association and component values (user: johnpateman, artifact: [1100d6e077], branch: Ver_3V3)
+ # 2017-05-07 [2d1e20f431] Initial commit (user: johnpateman, artifact: [24336219cc], branch: trunk)
+ # NOTE 'fossil -finfo -b' looks like this
+ # 21d331ea6b 2017-05-19 johnpate Ver_3V3 Preliminary work on CvPCB association a
+ # 2d1e20f431 2017-05-07 johnpate trunk Initial commit
+ # TODO Consider parsing the output of fossil finfo and split off date, artifactID, mesage, user and branch
+
+ fossilCmd = 'cd ' + path + ' && ' + fossilProg + ' finfo -b ' + kicadPCB
+
+ fossil = Popen(
+ fossilCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, _ = fossil.communicate()
+ fossil.wait()
+ line = (stdout.decode('utf-8').splitlines())
+ # fArtifacts = [a.replace(' ', ' ', 4) for a in line]
+ fArtifacts = [a.replace(' ', '\t', 4) for a in line]
+ return fArtifacts
+
+
+def gitDiff(path, kicadPCB):
+ '''Returns list of Git artifacts from a directory containing a
+ *.kicad_pcb file.'''
+
+ gitCmd = 'cd ' + path + ' && ' + gitProg + ' log --pretty=format:"%h \t %s"'
+ git = Popen(
+ gitCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, _ = git.communicate()
+ git.wait()
+ gArtifacts = (stdout.decode('utf-8').splitlines())
+ return gArtifacts
+
+
+def svnDiff(path, kicadPCB):
+ '''Returns list of SVN resvisions from a directory containing a
+ *.kicad_pcb file.'''
+ svnCmd = 'cd ' + path + ' && ' + svnProg + ' log -r HEAD:0 | perl -l40pe "s/^-+/\n/"'
+
+ svn = Popen(
+ svnCmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = svn.communicate()
+ svn.wait()
+ sArtifacts = (stdout.decode('utf-8').splitlines())
+ sArtifacts = list(filter(None, sArtifacts))
+ return sArtifacts
+
+
+def makeSVG(d1, d2, prjctName, prjctPath):
+ '''Hands off required .kicad_pcb files to "plotPCB2.py"
+ and generate .svg files. Routine is
+ v quick so all layers are plotted to svg.'''
+
+ print("Generating .svg files")
+
+ d1 = d1[:6]
+ d2 = d2[:6]
+
+ Diff1 = prjctPath + plotDir + '/' + d1 + '/' + prjctName
+ Diff2 = prjctPath + plotDir + '/' + d2 + '/' + prjctName
+
+ d1SVG = prjctPath + plotDir + '/' + d1
+ d2SVG = prjctPath + plotDir + '/' + d2
+
+ if not os.path.exists(d1SVG):
+ os.makedirs(d1SVG)
+ if not os.path.exists(d2SVG):
+ os.makedirs(d2SVG)
+
+ plot1Cmd = plotProg + ' ' + _escape_string(Diff1) + ' ' + _escape_string(d1SVG)
+ plot2Cmd = plotProg + ' ' + _escape_string(Diff2) + ' ' + _escape_string(d2SVG)
+
+ plot1=Popen(
+ plot1Cmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = plot1.communicate()
+ plotDims1 = (stdout.decode('utf-8').splitlines())
+
+
+ plot2=Popen(
+ plot2Cmd,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = plot2.communicate()
+ plotDims2 = (stdout.decode('utf-8').splitlines())
+
+ plot1.wait(); plot2.wait()
+
+
+ return (d1, d2, plotDims1[0], plotDims2[0])
+
+
+def makeSupportFiles(prjctName, prjctPath):
+ '''
+ Setup web directories for output
+ '''
+
+ webd = prjctPath + plotDir + webDir
+ webIndex = webd + '/index.html'
+ webStyle = webd + '/style.css'
+
+ if not os.path.exists(webd):
+ os.makedirs(webd)
+ os.makedirs(webd + '/tryptych')
+
+ makeCSS = open(webStyle, 'w')
+ makeCSS.write(css)
+
+ if os.path.exists(webIndex):
+ os.remove(webIndex)
+
+ return
+
+def getBoardData(board):
+ '''Takes a board reference and returns the
+ basic parameters from it.
+ Might be safer to split off the top section
+ before the modules to avoid the possibility of
+ recyling keywords like 'title' '''
+
+ prms = {
+ 'title': "",
+ 'rev': "",
+ 'company': "",
+ 'date': "",
+ 'page': "",
+ 'thickness': 0,
+ 'drawings': 0,
+ 'tracks': 0,
+ 'zones': 0,
+ 'modules': 0,
+ 'nets': 0
+ }
+
+ thickDone = False
+
+ with open(board, 'r') as f:
+ for line in f:
+ words = line.strip("\t ()").split()
+ for key in prms:
+ if len(words) > 1:
+ if key == words[0]:
+ complete =""
+ for i in range(1,len(words)):
+ complete += words[i].strip("\t ()").replace("\"","") + " "
+ prms[key] = complete
+ print(prms)
+ return(prms)
+
+def makeOutput(diffDir1, diffDir2, prjctName, prjctPath, times, dim1, dim2):
+ '''Write out HTML using template. Iterate through files in diff directories, generating
+ thumbnails and three way view (tryptych) page.
+ '''
+ webd = prjctPath + plotDir + webDir
+
+ board1 = prjctPath + plotDir + "/" + diffDir1 + "/" + prjctName
+ board2 = prjctPath + plotDir + "/" + diffDir2 + "/" + prjctName
+
+ webIndex = webd + '/index.html'
+
+ webOut = open(webIndex, 'w')
+
+ D1DATE, D1TIME, D2DATE, D2TIME = times.split(" ")
+
+ board_1_Info = getBoardData(board1)
+ board_2_Info = getBoardData(board2)
+
+ TITLE = board_1_Info.get('title')
+ DATE = board_1_Info.get('date')
+ COMPANY = board_1_Info.get('company')
+
+ THICK1 = board_1_Info.get('thickness')
+ DRAWINGS1 = board_1_Info.get('drawings')
+ TRACKS1 = board_1_Info.get('tracks')
+ ZONES1 = board_1_Info.get('zones')
+ MODULES1 = board_1_Info.get('modules')
+ NETS1 = board_1_Info.get('nets')
+
+ THICK2 = board_2_Info.get('thickness')
+ DRAWINGS2 = board_2_Info.get('drawings')
+ TRACKS2 = board_2_Info.get('tracks')
+ ZONES2 = board_2_Info.get('zones')
+ MODULES2 = board_2_Info.get('modules')
+ NETS2 = board_2_Info.get('nets')
+
+
+ index=indexHead.format(
+ TITLE=TITLE,
+ DATE=DATE,
+ COMPANY=COMPANY,
+ diffDir1=diffDir1,
+ diffDir2=diffDir2,
+ THICK1=THICK1,
+ THICK2=THICK2,
+ D1DATE=D1DATE,
+ D2DATE=D2DATE,
+ DRAWINGS1=DRAWINGS1,
+ DRAWINGS2=DRAWINGS2,
+ D1TIME=D1TIME,
+ D2TIME=D2TIME,
+ TRACKS1=TRACKS1,
+ TRACKS2=TRACKS2,
+ ZONES1=ZONES1,
+ ZONES2=ZONES2,
+ MODULES1=MODULES1,
+ MODULES2=MODULES2,
+ NETS1=NETS1,
+ NETS2=NETS2,
+ )
+
+ webOut.write(index)
+
+ diffCmnd1 = ()
+
+ source = prjctPath + plotDir + "/" + diffDir1 + "/"
+
+ tryptychDir = prjctPath + plotDir + webDir + '/tryptych'
+
+ if not os.path.exists(tryptychDir):
+ os.makedirs(tryptychDir)
+
+ # diffs = os.fsencode(source)
+
+ for f in os.listdir(source):
+ filename = os.fsdecode(f)
+ if filename.endswith(".svg"):
+ print(filename)
+ file, file_extension = os.path.splitext(filename)
+ tryptych = tryptychDir + '/' + file + '.html'
+ *project, layer = filename.split('-')
+ layer, ext = layer.split('.')
+ prjct, ext = filename.split('.')
+ # Accounts for project names containing hyphens
+ splitted = prjct.split('-')
+ prj = splitted[-2]
+ layer = splitted[-1]
+ out=outfile.format(
+ diff1=diffDir1,
+ diff2=diffDir2,
+ dim1=dim1,
+ dim2=dim2,
+ layer=layer,
+ layername=filename,
+ prj=prj)
+
+ webOut.write(out)
+
+ tryptychOut = open(tryptych, 'w')
+
+ t_out = tryptychHTML.format(
+ layername=filename,
+ diff1=diffDir1,
+ diff2=diffDir2,
+ dim1=dim1,
+ dim2=dim2,
+ plotDir=plotDir,
+ layer=layer,
+ prj=prj)
+
+ tryptychOut.write(t_out)
+
+ diffbase=diffProg+'{prjctPath}{plotDir}/{diff2}/*.kicad_pcb {prjctPath}{plotDir}/{diff1}/*.kicad_pcb >> {prjctPath}{plotDir}/diff.txt'
+
+ if not diffCmnd1:
+ diffCmnd1 = diffbase.format(
+ plotDir=plotDir,
+ diff1=diffDir1,
+ diff2=diffDir2,
+ prjctPath=_escape_string(prjctPath))
+ # print(diffCmnd1)
+
+ diff1Txt = Popen(
+ diffCmnd1,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = diff1Txt.communicate()
+ diff1Txt.wait()
+ #sed -e 's/(layer {mod}*)//g' |
+ mod = layer.replace("_",".")
+ # diffCmnd2 = diffProg + ''' --suppress-common-lines {prjctPath}{plotDir}/{diff2}/*.kicad_pcb {prjctPath}{plotDir}/{diff1}/*.kicad_pcb | grep {mod} | sed 's/> /<\/div>
/g' | sed 's/< /<\/div>
/g' | sed 's/\/n/<\/div>/g' | sed 's/(status [1-9][0-9])//g' '''.format(
+ diffCmnd2 = diffProg + ''' --suppress-common-lines {prjctPath}{plotDir}/{diff2}/*.kicad_pcb {prjctPath}{plotDir}/{diff1}/*.kicad_pcb | {grepProg} {mod} | sed 's/(status [1-9][0-9])//g' '''.format(
+ layername=filename,
+ plotDir=plotDir,
+ diff1=diffDir1,
+ diff2=diffDir2,
+ prjctPath=_escape_string(prjctPath),
+ mod=mod,
+ grepProg=grepProg,
+ webDir=webDir)
+
+
+ diff2Txt = Popen(
+ diffCmnd2,
+ shell=True,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True)
+ stdout, stderr = diff2Txt.communicate()
+ diff2Txt.wait()
+ out = stdout.decode('utf8')
+
+ processed = processDiff(out, mod)
+ processed+=twopane
+
+ tryptychOut.write(processed)
+ webOut.write(tail)
+
+def processDiff(diffText, mod):
+
+ keywords=[
+ ("(module ","Modules",("Component","Reference","Timestamp")),
+ ("(gr_text ","Text",("Text","Position")),
+ ("(via ","Vias",("Coordinate","Size","Drill","Layers","Net")),
+ ("(fp_text ","FP Text",("Reference","Coordinate")),
+ ("(pad ","Pads",("Number","Type","Shape","Coordinate","Size","Layers","Ratio")),
+ ("(gr_line ","Graphics",("Start","End ","Width","Net")),
+ ("(fp_arc","Arcs",("Start","End ","Angle","Width")),
+ ("(segment","Segments",("Start","End ","Width","Net","Timestamp")),
+ ("(fp_circle","Circles",("Centre","End ","Width")),
+ ]
+
+ d={
+ "\(start ":"
",
+ "\(end ":" ",
+ "\(width ":" ",
+ "\(tedit ":" ",
+ "\(tstamp ":" ",
+ "\(at ":" ",
+ "\(size ":" ",
+ "\(drill ":" ",
+ "\(layers ":" ",
+ "\(net ":" ",
+ "\(roundrect_rratio ":" ",
+ "\(angle ":" ",
+ "\(center ":" ",
+ "\)":" ",
+ "user (\w+)":r'
\1 ',
+ "reference (\w+)":r'
\1 ',
+ "([0-9]) smd":r'
\1 Surface ',
+ "roundrect":"
Rounded ",
+ "rect":"
Rectangle ",
+ "(\w.+):(\w.+)":r'
\1 \2 ',
+ "(?<=\")(.*)(?=\")":r'
\1 ',
+ "[\"]":r'',
+ "[**]":r'',
+ }
+
+ final =""
+ content = ""
+ output = ""
+ combined = ""
+ header = ""
+ tbL = ""
+ tbR = ""
+ checked = "checked"
+
+
+ top1='''
'''
+ tsl='''
+
+
'''
+ tsr='''
+
+
'''
+ clearfix ='''
+
+
+
'''
+
+
+
+ for indx,layerInfo in enumerate(keywords):
+ combined = tbL = tbR = ""
+ for indx2,parameter in enumerate(layerInfo[2]):
+ tbR = tbR + "" + parameter + " "
+ tbL = tbL + "" + parameter + " "
+ for line in diffText.splitlines():
+ if ((layerInfo[0] in line) and (mod in line)):
+ output = line.replace(layerInfo[0], "")
+ output = output.replace("(layer " + mod + ")", "")
+ # print(output)
+ for item in d.keys():
+ output = re.sub(item, d[item], output)
+
+ if output.count("") == indx2:
+ output += " "
+ if output == "":
+ output = ""
+ output += ""
+ # print(output)
+
+ if output[0]==">":
+ tbL = tbL + " " + output[1:]
+ elif output[0] == "<":
+ tbR = tbR + " " + output[1:]
+
+ combined = tsl + tbL + "
" + tsr + tbR + "
"
+ content = top1.format(tabn=indx,content=combined,label=layerInfo[1],checked=checked)
+ checked=""
+
+ final = final + content
+ final = "
"+ final + "
" + clearfix
+ return(final)
+
+
+def popup_showinfo(progress):
+ display = 'Processing: ' + progress
+ p = Label(gui, Text=display)
+ p.pack()
+
+def scmAvailable():
+ SCMS = ''
+ if (fossilProg != ''):
+ SCMS = SCMS + "Fossil \n"
+ if (gitProg != ''):
+ SCMS = SCMS + "Git \n"
+ if (svnProg != ''):
+ SCMS = SCMS + "SVN "
+
+ return (SCMS)
+
+
+class Handler(http.server.SimpleHTTPRequestHandler):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, directory=os.path.realpath(prjctPath + plotDir), **kwargs)
+
+class Select(tk.Toplevel):
+ def __init__(self, parent):
+ tk.Toplevel.__init__(self, parent)
+ tk.Toplevel.withdraw(self)
+ tk.Toplevel.update(self)
+ action = messagebox.askokcancel(
+ self,
+ message="Select a *.kicad_pcb file under version control",
+ detail="Available: \n\n" + SCMS)
+ self.update()
+ if action == "cancel":
+ self.quit()
+
+
+def startWebServer():
+ with socketserver.TCPServer(("", PORT), Handler) as httpd:
+ print("serving at port", PORT)
+ webbrowser.open('http://127.0.0.1:' + str(PORT) + '/web/index.html')
+ httpd.serve_forever()
+
+
+if __name__ == "__main__":
+
+ SCMS = scmAvailable()
+
+ if (SCMS == ""):
+ print("You need to have at least one SCM program path identified in lines 32 - 40")
+ exit()
+ gui = tk.Tk(':0.0', SCMS)
+ gui.withdraw()
+ gui.update()
+ Select = Select(gui)
+ Select.destroy()
+ prjctPath, prjctName = getProject()
+ gui.update()
+ gui.deiconify()
+
+ scm = getSCM(_escape_string(prjctPath))
+ gui.destroy()
+
+
+ if scm == 'Git':
+ artifacts = gitDiff(_escape_string(prjctPath), prjctName)
+ if scm == 'Fossil':
+ artifacts = fossilDiff(_escape_string(prjctPath), prjctName)
+ if scm == 'SVN':
+ artifacts = svnDiff(_escape_string(prjctPath), prjctName)
+ if scm == '':
+ print("This project is either not under version control or you have not set the path to the approriate SCM program in lines 32-40")
+ sys.exit(0)
+
+
+ d1, d2 = tkUI.runGUI(artifacts, prjctName, prjctPath, 'Git')
+
+ print("Commit1", d1)
+ print("Commit2", d2)
+
+ if scm == 'Git':
+ times = getGitDiff(d1, d2, prjctName, prjctPath)
+ if scm == 'Fossil':
+ times = getFossilDiff(d1, d2, prjctName, prjctPath)
+ if scm == 'SVN':
+ a1, *tail = d1.split(' |')
+ d1 = a1[1:]
+ a2, *tail = d2.split(' |')
+ d2 = a2[1:]
+ times = getSVNDiff(d1, d2, prjctName, prjctPath)
+
+
+ svgDir1, svgDir2, boardDims1, boardDims2 = makeSVG(d1, d2, prjctName, prjctPath)
+
+ makeSupportFiles(prjctName, prjctPath)
+
+ makeOutput(svgDir1, svgDir2, prjctName, prjctPath, times, boardDims1, boardDims2)
+
+ startWebServer()
+
+ webbrowser.open(
+ 'http://127.0.0.1:' + str(PORT) + '/web/index.html')
diff --git a/plotPCB.py b/plotPCB.py
index 086087d..4b44b02 100755
--- a/plotPCB.py
+++ b/plotPCB.py
@@ -5,6 +5,7 @@ Kicad plot pcb file.
Plot variety of svg files in plot directory
'''
+import sys
import pcbnew
from pcbnew import *