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 = ''' +
+ +
+''' + +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'\1Surface', + "roundrect":"Rounded", + "rect":"Rectangle", + "(\w.+):(\w.+)":r'\1 \2', + "(?<=\")(.*)(?=\")":r'\1', + "[\"]":r'', + "[**]":r'', + } + + final ="" + content = "" + output = "" + combined = "" + header = "" + tbL = "" + tbR = "" + checked = "checked" + + + top1='''

{content}
''' + tsl='''
+
+ ''' + tsr='''
+
+
''' + clearfix ='''
+
+
+
''' + + + + for indx,layerInfo in enumerate(keywords): + combined = tbL = tbR = "" + for indx2,parameter in enumerate(layerInfo[2]): + tbR = tbR + "" + tbL = tbL + "" + 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("" + if output == "" + # print(output) + + if output[0]==">": + tbL = tbL + "" + output[1:] + elif output[0] == "<": + tbR = tbR + "" + output[1:] + + combined = tsl + tbL + "
" + parameter + "" + parameter + "") == indx2: + output += "": + output = "" + output += "
" + 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 *