mirror of https://github.com/rusefi/KiCad-Diff.git
1601 lines
44 KiB
Python
Executable File
1601 lines
44 KiB
Python
Executable File
#!/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 <div> 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
|
|
|
|
# -------------------------------------------------------------------------
|
|
# 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 = '/usr/local/bin/fossil'
|
|
svnProg = '/usr/bin/svn'
|
|
plotDir = '/Plots'
|
|
webDir = '/web'
|
|
diffProg = '/usr/bin/diff'
|
|
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 = 9091
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
# 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 = '''
|
|
<div class="clearfix"></div>
|
|
<div style="padding:6px;"></div>
|
|
'''
|
|
|
|
indexHead = '''
|
|
<!DOCTYPE HTML>
|
|
<html lang="en">
|
|
<meta charset="utf-8" />
|
|
<head>
|
|
<link rel="stylesheet" type="text/css" href="style.css" media="screen" />
|
|
</head>
|
|
<div class="responsivefull">
|
|
<table style="border-color: #aaaaaa; width: 100%; height: 2px;" border="2px" cellspacing="2px" cellpadding="3px">
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="3" rowspan="3" width="45%">
|
|
<div class="title"> Title: {TITLE} </div>
|
|
<div class="details"> Company: {COMPANY} </div>
|
|
</td>
|
|
<td width="25%">
|
|
<div class="versions">Thickness (mm)</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions green">{THICK1}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions red">{THICK2}</div>
|
|
</td>
|
|
</tr>
|
|
<td width="25%">
|
|
<div class="versions">Modules</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions green">{MODULES1}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions red">{MODULES2}</div>
|
|
</td>
|
|
<tr>
|
|
<td width="25%">
|
|
<div class="versions">Drawings</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions green">{DRAWINGS1}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions red">{DRAWINGS2}</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td width="15%">
|
|
<div class="versions">Version</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions green">{diffDir1}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions red">{diffDir2}</div>
|
|
</td>
|
|
<td width="25%">
|
|
<div class="versions">Nets</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions green">{NETS1}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions red">{NETS2}</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td width="15%">
|
|
<div class="versions">Date</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions">{D1DATE}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions">{D2DATE}</div>
|
|
</td>
|
|
<td width="25%">
|
|
<div class="versions">Tracks</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions green">{TRACKS1}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions red">{TRACKS2}</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td width="15%">
|
|
<div class="versions">Time</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions">{D1TIME}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions">{D2TIME}</div>
|
|
</td>
|
|
<td width="25%">
|
|
<div class="versions">Zones</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions green">{ZONES1}</div>
|
|
</td>
|
|
<td width="15%">
|
|
<div class="versions red">{ZONES2}</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
'''
|
|
|
|
outfile = '''
|
|
<div class="responsive">
|
|
<div class="gallery">
|
|
<a target="_blank" href=../{diff1}/{layername}>
|
|
<a href=./tryptych/{prj}-{layer}.html> <img class="{layer}" src=../{diff1}/{layername} height="200"> </a>
|
|
</a>
|
|
<div class="desc">{layer}</div>
|
|
</div>
|
|
</div>
|
|
'''
|
|
|
|
tryptychHTML = '''
|
|
<!DOCTYPE HTML>
|
|
<html lang="en">
|
|
<meta charset="utf-8" />
|
|
<head>
|
|
<link rel="stylesheet" type="text/css" href="../style.css" media="screen" />
|
|
<style>
|
|
div.responsive {{
|
|
padding: 0 6px;
|
|
float: left;
|
|
width: 49.99%;
|
|
}}
|
|
</style>
|
|
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.0/dist/svg-pan-zoom.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<div class="title">{prj}</div>
|
|
<div class="subtitle">{layer}</div>
|
|
|
|
|
|
<div id="compo-container" style="width: 100%; height: 800px;">
|
|
<svg id="svg-id" xmlns="http://www.w3.org/2000/svg" style="display: inline; width: inherit; min-width: inherit; max-width: inherit; height: inherit; min-height: inherit; max-height: inherit;" version="1.1">
|
|
<g>
|
|
<svg id="compo">
|
|
<defs>
|
|
<filter id="f1">
|
|
<feColorMatrix id="c1" type="matrix" values="1 0 0 0 0
|
|
0 1 0 1 0
|
|
0 0 1 1 0
|
|
0 0 0 1 0 " />
|
|
</filter>
|
|
</defs>
|
|
<image x="0" y="0" height="100%" width="100%" filter="url(#f1)" xlink:href="../../{diff1}/{layername}" />
|
|
</svg>
|
|
|
|
<svg id="compo2">
|
|
<defs>
|
|
<filter id="f2">
|
|
<feColorMatrix id="c2" type="matrix" values="1 0 0 1 0
|
|
0 1 0 0 0
|
|
0 0 1 0 0
|
|
0 0 0 .5 0" />
|
|
</filter>
|
|
</defs>
|
|
<image x="0" y="0" height="100%" width="100%" filter="url(#f2)" xlink:href="../../{diff2}/{layername}" />
|
|
</svg>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
|
|
<div id="sbs-container" width=100%; height=100% >
|
|
<embed id="diff1" class="{layer}" type="image/svg+xml" style="width: 50%; float: left; border:1px solid black;" src="../../{diff1}/{layername}" />
|
|
<embed id="diff2" class="{layer}" type="image/svg+xml" style="width: 50%; border:1px solid black;" src="../../{diff2}/{layername}" />
|
|
</div>
|
|
|
|
'''
|
|
|
|
twopane='''
|
|
<script>
|
|
// Don't use window.onLoad like this in production, because it can only listen to one function.
|
|
window.onload = function() {
|
|
// Expose variable for testing purposes
|
|
window.panZoomDiff = svgPanZoom('#svg-id', {
|
|
zoomEnabled: true,
|
|
controlIconsEnabled: true,
|
|
center: true,
|
|
minZoom: 1.5,
|
|
maxZoom: 20,
|
|
});
|
|
// Expose variable to use for testing
|
|
window.zoomDiff = svgPanZoom('#diff1', {
|
|
zoomEnabled: true,
|
|
controlIconsEnabled: true,
|
|
minZoom: 1.5,
|
|
maxZoom: 20,
|
|
// Uncomment this in order to get Y asis synchronized pan
|
|
// beforePan: function(oldP, newP) {return {y:false}},
|
|
});
|
|
|
|
// Expose variable to use for testing
|
|
window.zoomDiff2 = svgPanZoom('#diff2', {
|
|
zoomEnabled: true,
|
|
controlIconsEnabled: true,
|
|
minZoom: 1.5,
|
|
maxZoom: 20,
|
|
});
|
|
|
|
zoomDiff.setOnZoom(function(level) {
|
|
zoomDiff2.zoom(level)
|
|
zoomDiff2.pan(zoomDiff.getPan())
|
|
})
|
|
|
|
zoomDiff.setOnPan(function(point) {
|
|
zoomDiff2.pan(point)
|
|
})
|
|
|
|
zoomDiff2.setOnZoom(function(level) {
|
|
zoomDiff.zoom(level)
|
|
zoomDiff.pan(zoomDiff2.getPan())
|
|
})
|
|
|
|
zoomDiff2.setOnPan(function(point) {
|
|
zoomDiff.pan(point)
|
|
})
|
|
|
|
};
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
'''
|
|
|
|
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 ' + 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'''
|
|
|
|
artifact1 = diff1[:6]
|
|
artifact2 = diff2[:6]
|
|
|
|
findDiff = 'cd ' + prjctPath + ' && ' + gitProg + ' diff --name-only ' + \
|
|
artifact1 + ' ' + artifact2 + ' | ' + grepProg + ' *.kicad_pcb'
|
|
|
|
print(findDiff)
|
|
|
|
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, prjctPath)
|
|
|
|
gitArtifact1 = 'cd ' + prjctPath + ' && ' + gitProg + ' show ' + artifact1 + \
|
|
':' + gitPath + ' > ' + outputDir1 + '/' + prjctName
|
|
|
|
gitArtifact2 = 'cd ' + prjctPath + ' && ' + gitProg + ' show ' + artifact2 + \
|
|
':' + gitPath + ' > ' + outputDir2 + '/' + prjctName
|
|
|
|
print(gitArtifact1, gitArtifact2)
|
|
|
|
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 ' + prjctPath + ' && ' + gitProg + ' show -s --format="%ci" ' + artifact1
|
|
gitDateTime2 = 'cd ' + 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 ' + prjctPath + ' && fossil diff --brief -r ' + \
|
|
artifact1 + ' --to ' + artifact2 + ' | grep .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 ' + prjctPath + ' && fossil cat ' + prjctPath + '/' + prjctName + \
|
|
' -r ' + artifact1 + ' > ' + outputDir1 + '/' + prjctName
|
|
fossilArtifact2 = 'cd ' + prjctPath + ' && fossil cat ' + prjctPath + '/' + prjctName + \
|
|
' -r ' + artifact2 + ' > ' + outputDir2 + '/' + prjctName
|
|
|
|
fossilInfo1 = 'cd ' + prjctPath + ' && fossil info ' + artifact1
|
|
fossilInfo2 = 'cd ' + 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/"'
|
|
print(svnCmd)
|
|
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 + ' ' + Diff1 + " " + d1SVG
|
|
plot2Cmd = plotProg + ' ' + Diff2 + " " + d2SVG
|
|
|
|
print(plot1Cmd,plot2Cmd)
|
|
|
|
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()
|
|
|
|
|
|
print(plotDims1, plotDims2)
|
|
|
|
|
|
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=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><div class="differences added">/g' | sed 's/< /<\/div><div class="differences removed">/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 | grep {mod} | sed 's/(status [1-9][0-9])//g' '''.format(
|
|
layername=filename,
|
|
plotDir=plotDir,
|
|
diff1=diffDir1,
|
|
diff2=diffDir2,
|
|
prjctPath=prjctPath,
|
|
mod=mod,
|
|
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 ":"<td>",
|
|
"\(end ":"<td>",
|
|
"\(width ":"<td>",
|
|
"\(tedit ":"<td>",
|
|
"\(tstamp ":"<td>",
|
|
"\(at ":"<td>",
|
|
"\(size ":"<td>",
|
|
"\(drill ":"<td>",
|
|
"\(layers ":"<td>",
|
|
"\(net ":"<td>",
|
|
"\(roundrect_rratio ":"<td>",
|
|
"\(angle ":"<td>",
|
|
"\(center ":"<td>",
|
|
"\)":"</td>",
|
|
"user (\w+)":r'<td>\1</td>',
|
|
"reference (\w+)":r'<td>\1</td>',
|
|
"([0-9]) smd":r'<td>\1</td><td>Surface</td>',
|
|
"roundrect":"<td>Rounded</td>",
|
|
"rect":"<td>Rectangle</td>",
|
|
"(\w.+):(\w.+)":r'<td>\1 \2</td>',
|
|
"(?<=\")(.*)(?=\")":r'<td>\1</td>',
|
|
"[\"]":r'',
|
|
"[**]":r'',
|
|
}
|
|
|
|
final =""
|
|
content = ""
|
|
output = ""
|
|
combined = ""
|
|
header = ""
|
|
tbL = ""
|
|
tbR = ""
|
|
checked = "checked"
|
|
|
|
|
|
top1='''<input name='tabbed' id='tabbed{tabn}' type='radio' {checked}><section><h1><label for='tabbed{tabn}'>{label}</label></h1><div>{content}</div></section>'''
|
|
tsl='''<div class='responsive'>
|
|
<div class = 'tbl'>
|
|
<table style="border-color: #aaaaaa; width: 100%; height: 2px;" border="2px" cellspacing="2px" cellpadding="3px">'''
|
|
tsr='''<div class='responsive'>
|
|
<div class = 'tbr'>
|
|
<table style="border-color: #aaaaaa; width: 100%; height: 2px;" border="2px" cellspacing="2px" cellpadding="3px">'''
|
|
clearfix ='''<div class='clearfix'>
|
|
</div>
|
|
<div style='padding:6px;'>
|
|
</div>'''
|
|
|
|
|
|
|
|
for indx,layerInfo in enumerate(keywords):
|
|
combined = tbL = tbR = ""
|
|
for indx2,parameter in enumerate(layerInfo[2]):
|
|
tbR = tbR + "<th>" + parameter + "</th>"
|
|
tbL = tbL + "<th>" + parameter + "</th>"
|
|
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("<td>") == indx2:
|
|
output += "<td></td>"
|
|
if output == "<td>":
|
|
output = ""
|
|
output += "</tr>"
|
|
# print(output)
|
|
|
|
if output[0]==">":
|
|
tbL = tbL + "<tr>" + output[1:]
|
|
elif output[0] == "<":
|
|
tbR = tbR + "<tr>" + output[1:]
|
|
|
|
combined = tsl + tbL + "</table></div></div>" + tsr + tbR + "</table></div></div>"
|
|
content = top1.format(tabn=indx,content=combined,label=layerInfo[1],checked=checked)
|
|
checked=""
|
|
|
|
final = final + content
|
|
final = "<div class = 'tabbed'>"+ final + "</div>" + 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()
|
|
print(SCMS)
|
|
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(prjctPath)
|
|
gui.destroy()
|
|
|
|
|
|
if scm == 'Git':
|
|
artifacts = gitDiff(prjctPath, prjctName)
|
|
if scm == 'Fossil':
|
|
artifacts = fossilDiff(prjctPath, prjctName)
|
|
if scm == 'SVN':
|
|
artifacts = svnDiff(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, scm)
|
|
|
|
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')
|