Refactor and clean up code (#39)

* Format all files using autopep8 to add basic PEP8 conformity.
* Add Exception types to bare excepts to prevent catching `ctrl+c`
* Remove some unnecessary assignment to dummy variables before returning
* Add `Optional` to type hints that can be `NoneType`
* Change a number of single-letter variables to more descriptive names
* Replace string.format() use with Python's f-strings, as they tends to be cleaner, and provide a performance boost.
  * One multiline string was left as string.format() as I do not believe f-strings support multiline
  * Some of the string.format() instances had unused/ignored arguments. I left them out of the f-strings, but I marked those cases with a comments that begins `# FIXME:`
* Rename variables that were shadowding python standard functions (specifically `format->fmt`, `input->inp`, `type->maintype`)
  * Some instances of `type` were not changed, as it breaks the yaml parsing. Needs to be looked into.
* Move classes in `wireviz.py` to two new files `DataClasses.py` and `Harness.py`.

Co-authored-by: Daniel Rojas <github@danielrojas.net>
This commit is contained in:
Gabe R 2020-06-29 04:39:34 -05:00 committed by Daniel Rojas
parent a3728f52ce
commit 82b173f2ce
5 changed files with 635 additions and 592 deletions

136
src/wireviz/DataClasses.py Normal file
View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Optional, List, Any
from dataclasses import dataclass, field
from wireviz.wv_helper import int2tuple
from wireviz import wv_colors
@dataclass
class Connector:
name: str
category: Optional[str] = None
type: Optional[str] = None
subtype: Optional[str] = None
pincount: Optional[int] = None
notes: Optional[str] = None
pinout: List[Any] = field(default_factory=list)
pinnumbers: List[Any] = field(default_factory=list)
color: Optional[str] = None
show_name: bool = True
show_pincount: bool = True
hide_disconnected_pins: bool = False
def __post_init__(self):
self.ports_left = False
self.ports_right = False
self.loops = []
self.visible_pins = {}
if self.pincount is None:
if self.pinout:
self.pincount = len(self.pinout)
elif self.pinnumbers:
self.pincount = len(self.pinnumbers)
elif self.category == 'ferrule':
self.pincount = 1
else:
raise Exception('You need to specify at least one, pincount, pinout or pinnumbers')
if self.pinout and self.pinnumbers:
if len(self.pinout) != len(self.pinnumbers):
raise Exception('Given pinout and pinnumbers size mismatch')
# create default lists for pinnumbers (sequential) and pinouts (blank) if not specified
if not self.pinnumbers:
self.pinnumbers = list(range(1, self.pincount + 1))
if not self.pinout:
self.pinout = [''] * self.pincount
def loop(self, from_pin, to_pin):
self.loops.append((from_pin, to_pin))
if self.hide_disconnected_pins:
self.visible_pins[from_pin] = True
self.visible_pins[to_pin] = True
def activate_pin(self, pin):
self.visible_pins[pin] = True
@dataclass
class Cable:
name: str
category: Optional[str] = None
type: Optional[str] = None
gauge: Optional[float] = None
gauge_unit: Optional[str] = None
show_equiv: bool = False
length: float = 0
wirecount: Optional[int] = None
shield: bool = False
notes: Optional[str] = None
colors: List[Any] = field(default_factory=list)
color_code: Optional[str] = None
show_name: bool = True
show_pinout: bool = False
show_wirecount: bool = True
def __post_init__(self):
if isinstance(self.gauge, str): # gauge and unit specified
try:
g, u = self.gauge.split(' ')
except Exception:
raise Exception('Gauge must be a number, or number and unit separated by a space')
self.gauge = g
self.gauge_unit = u.replace('mm2', 'mm\u00B2')
elif self.gauge is not None: # gauge specified, assume mm2
if self.gauge_unit is None:
self.gauge_unit = 'mm\u00B2'
else:
pass # gauge not specified
self.connections = []
if self.wirecount: # number of wires explicitly defined
if self.colors: # use custom color palette (partly or looped if needed)
pass
elif self.color_code: # use standard color palette (partly or looped if needed)
if self.color_code not in wv_colors.COLOR_CODES:
raise Exception('Unknown color code')
self.colors = wv_colors.COLOR_CODES[self.color_code]
else: # no colors defined, add dummy colors
self.colors = [''] * self.wirecount
# make color code loop around if more wires than colors
if self.wirecount > len(self.colors):
m = self.wirecount // len(self.colors) + 1
self.colors = self.colors * int(m)
# cut off excess after looping
self.colors = self.colors[:self.wirecount]
else: # wirecount implicit in length of color list
if not self.colors:
raise Exception('Unknown number of wires. Must specify wirecount or colors (implicit length)')
self.wirecount = len(self.colors)
# for BOM generation
self.wirecount_and_shield = (self.wirecount, self.shield)
def connect(self, from_name, from_pin, via_pin, to_name, to_pin):
from_pin = int2tuple(from_pin)
via_pin = int2tuple(via_pin)
to_pin = int2tuple(to_pin)
if len(from_pin) != len(to_pin):
raise Exception('from_pin must have the same number of elements as to_pin')
for i, _ in enumerate(from_pin):
# self.connections.append((from_name, from_pin[i], via_pin[i], to_name, to_pin[i]))
self.connections.append(Connection(from_name, from_pin[i], via_pin[i], to_name, to_pin[i]))
@dataclass
class Connection:
from_name: Any
from_port: Any
via_port: Any
to_name: Any
to_port: Any

332
src/wireviz/Harness.py Normal file
View File

@ -0,0 +1,332 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from wireviz.DataClasses import Connector, Cable
from graphviz import Graph
from wireviz import wv_colors
from wireviz.wv_helper import awg_equiv, tuplelist2tsv, nested, flatten2d
from collections import Counter
from typing import List
class Harness:
def __init__(self):
self.color_mode = 'SHORT'
self.connectors = {}
self.cables = {}
def add_connector(self, name, *args, **kwargs):
self.connectors[name] = Connector(name, *args, **kwargs)
def add_cable(self, name, *args, **kwargs):
self.cables[name] = Cable(name, *args, **kwargs)
def loop(self, connector_name, from_pin, to_pin):
self.connectors[connector_name].loop(from_pin, to_pin)
def connect(self, from_name, from_pin, via_name, via_pin, to_name, to_pin):
self.cables[via_name].connect(from_name, from_pin, via_pin, to_name, to_pin)
if from_name in self.connectors:
self.connectors[from_name].activate_pin(from_pin)
if to_name in self.connectors:
self.connectors[to_name].activate_pin(to_pin)
def create_graph(self):
dot = Graph()
dot.body.append('// Graph generated by WireViz')
dot.body.append('// https://github.com/formatc1702/WireViz')
font = 'arial'
dot.attr('graph', rankdir='LR',
ranksep='2',
bgcolor='white',
nodesep='0.33',
fontname=font)
dot.attr('node', shape='record',
style='filled',
fillcolor='white',
fontname=font)
dot.attr('edge', style='bold',
fontname=font)
# prepare ports on connectors depending on which side they will connect
for _, cable in self.cables.items():
for connection in cable.connections:
if connection.from_port is not None: # connect to left
self.connectors[connection.from_name].ports_right = True
if connection.to_port is not None: # connect to right
self.connectors[connection.to_name].ports_left = True
for key, connector in self.connectors.items():
if connector.category == 'ferrule':
subtype = f', {connector.subtype}' if connector.subtype else ''
color = wv_colors.translate_color(connector.color, self.color_mode) if connector.color else ''
infostring = f'{connector.type}{subtype} {color}'
infostring_l = infostring if connector.ports_right else ''
infostring_r = infostring if connector.ports_left else ''
# INFO: Leaving this one as a string.format form because f-strings do not work well with triple quotes
colorbar = f'<TD BGCOLOR="{wv_colors.translate_color(connector.color, "HEX")}" BORDER="1" SIDES="LR" WIDTH="4"></TD>' if connector.color else ''
dot.node(key, shape='none',
style='filled',
margin='0',
orientation='0' if connector.ports_left else '180',
label='''<
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="2"><TR>
<TD PORT="p1l"> {infostring_l} </TD>
{colorbar}
<TD PORT="p1r"> {infostring_r} </TD>
</TR></TABLE>
>'''.format(infostring_l=infostring_l, infostring_r=infostring_r, colorbar=colorbar))
else: # not a ferrule
attributes = [connector.type,
connector.subtype,
f'{connector.pincount}-pin' if connector.show_pincount else'']
pinouts = [[], [], []]
for pinnumber, pinname in zip(connector.pinnumbers, connector.pinout):
if connector.hide_disconnected_pins and not connector.visible_pins.get(pinnumber, False):
continue
pinouts[1].append(pinname)
if connector.ports_left:
pinouts[0].append(f'<p{pinnumber}l>{pinnumber}')
if connector.ports_right:
pinouts[2].append(f'<p{pinnumber}r>{pinnumber}')
label = [connector.name if connector.show_name else '', attributes, pinouts, connector.notes]
dot.node(key, label=nested(label))
if len(connector.loops) > 0:
dot.attr('edge', color='#000000:#ffffff:#000000')
if connector.ports_left:
loop_side = 'l'
loop_dir = 'w'
elif connector.ports_right:
loop_side = 'r'
loop_dir = 'e'
else:
raise Exception('No side for loops')
for loop in connector.loops:
# FIXME: Original string.format style had some unused arguments (port_to for 1st arg,
# port_from for 2nd arg). De we need them back?
dot.edge(f'{connector.name}:p{loop[0]}{loop_side}:{loop_dir}',
f'{connector.name}:p{loop[1]}{loop_side}:{loop_dir}')
for _, cable in self.cables.items():
awg_fmt = f' ({awg_equiv(cable.gauge)} AWG)' if cable.gauge_unit == 'mm\u00B2' and cable.show_equiv else ''
attributes = [f'{len(cable.colors)}x' if cable.show_wirecount else '',
f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else '', # TODO: show equiv
'+ S' if cable.shield else '',
f'{cable.length} m' if cable.length > 0 else '']
attributes = list(filter(None, attributes))
html = '<table border="0" cellspacing="0" cellpadding="0"><tr><td>' # main table
html = f'{html}<table border="0" cellspacing="0" cellpadding="3" cellborder="1">' # name+attributes table
if cable.show_name:
html = f'{html}<tr><td colspan="{len(attributes)}">{cable.name}</td></tr>'
html = f'{html}<tr>' # attribute row
for attrib in attributes:
html = f'{html}<td>{attrib}</td>'
html = f'{html}</tr>' # attribute row
html = f'{html}</table></td></tr>' # name+attributes table
html = f'{html}<tr><td>&nbsp;</td></tr>' # spacer between attributes and wires
html = f'{html}<tr><td><table border="0" cellspacing="0" cellborder="0">' # conductor table
for i, connection in enumerate(cable.colors, 1):
p = []
p.append(f'<!-- {i}_in -->')
p.append(wv_colors.translate_color(connection, self.color_mode))
p.append(f'<!-- {i}_out -->')
html = f'{html}<tr>'
for bla in p:
html = f'{html}<td>{bla}</td>'
html = f'{html}</tr>'
bgcolor = wv_colors.translate_color(connection, 'hex')
bgcolor = bgcolor if bgcolor != '' else '#ffffff'
html = f'{html}<tr><td colspan="{len(p)}" cellpadding="0" height="6" bgcolor="{bgcolor}" border="2" sides="tb" port="w{i}"></td></tr>'
if cable.shield:
p = ['<!-- s_in -->', 'Shield', '<!-- s_out -->']
html = f'{html}<tr><td>&nbsp;</td></tr>' # spacer
html = f'{html}<tr>'
for bla in p:
html = html + f'<td>{bla}</td>'
html = f'{html}</tr>'
# FIXME, original string.format had a unused bgcolor argument. Do we need it back
html = f'{html}<tr><td colspan="{len(p)}" cellpadding="0" height="6" border="2" sides="b" port="ws"></td></tr>'
html = f'{html}<tr><td>&nbsp;</td></tr>' # spacer at the end
html = f'{html}</table>' # conductor table
html = f'{html}</td></tr>' # main table
if cable.notes:
html = f'{html}<tr><td cellpadding="3">{cable.notes}</td></tr>' # notes table
html = f'{html}<tr><td>&nbsp;</td></tr>' # spacer at the end
html = f'{html}</table>' # main table
# connections
for connection in cable.connections:
if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield
search_color = cable.colors[connection.via_port - 1]
if search_color in wv_colors.color_hex:
dot.attr('edge', color=f'#000000:{wv_colors.color_hex[search_color]}:#000000')
else: # color name not found
dot.attr('edge', color='#000000:#ffffff:#000000')
else: # it's a shield connection
dot.attr('edge', color='#000000')
if connection.from_port is not None: # connect to left
from_ferrule = self.connectors[connection.from_name].category == 'ferrule'
port = f':p{connection.from_port}r' if not from_ferrule else ''
code_left_1 = f'{connection.from_name}{port}:e'
# FIXME: Uncomment, then add to end of f-string if needed
# via_subport = 'i' if c.show_pinout else ''
code_left_2 = f'{cable.name}:w{connection.via_port}:w'
dot.edge(code_left_1, code_left_2)
from_string = f'{connection.from_name}:{connection.from_port}' if not from_ferrule else ''
html = html.replace(f'<!-- {connection.via_port}_in -->', from_string)
if connection.to_port is not None: # connect to right
to_ferrule = self.connectors[connection.to_name].category == 'ferrule'
# FIXME: Add in if it was supposed to be here. the add to fstring two lines down
# via_subport = 'o' if c.show_pinout else ''
code_right_1 = f'{cable.name}:w{connection.via_port}:e'
to_port = f':p{connection.to_port}l' if not to_ferrule else ''
code_right_2 = f'{connection.to_name}{to_port}:w'
dot.edge(code_right_1, code_right_2)
to_string = f'{connection.to_name}:{connection.to_port}' if not to_ferrule else ''
html = html.replace(f'<!-- {connection.via_port}_out -->', to_string)
dot.node(cable.name, label=f'<{html}>', shape='box',
style='filled,dashed' if cable.category == 'bundle' else '', margin='0', fillcolor='white')
return dot
def output(self, filename, directory='_output', view=False, cleanup=True, fmt='pdf', gen_bom=False):
# graphical output
digraph = self.create_graph()
for f in fmt:
digraph.format = f
digraph.render(filename=filename, directory=directory, view=view, cleanup=cleanup)
digraph.save(filename=f'{filename}.gv', directory=directory)
# bom output
bom_list = self.bom_list()
with open(f'{filename}.bom.tsv', 'w') as file:
file.write(tuplelist2tsv(bom_list))
# HTML output
with open(f'{filename}.html', 'w') as file:
file.write('<html><body style="font-family:Arial">')
file.write('<h1>Diagram</h1>')
with open(f'{filename}.svg') as svg:
for svgdata in svg:
file.write(svgdata)
file.write('<h1>Bill of Materials</h1>')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">')
file.write('<tr>')
for item in listy[0]:
file.write(f'<th align="left" style="border:1px solid #000000; padding: 8px">{item}</th>')
file.write('</tr>')
for row in listy[1:]:
file.write('<tr>')
for i, item in enumerate(row):
align = 'align="right"' if listy[0][i] == 'Qty' else ''
file.write(f'<td {align} style="border:1px solid #000000; padding: 4px">{item}</td>')
file.write('</tr>')
file.write('</table>')
file.write('</body></html>')
def bom(self):
bom = []
bom_connectors = []
bom_cables = []
# connectors
types = Counter([(v.type, v.subtype, v.pincount) for v in self.connectors.values()])
for maintype in types:
items = {k: v for k, v in self.connectors.items() if (v.type, v.subtype, v.pincount) == maintype}
shared = next(iter(items.values()))
designators = list(items.keys())
designators.sort()
conn_type = f', {shared.type}' if shared.type else ''
conn_subtype = f', {shared.subtype}' if shared.subtype else ''
conn_pincount = f', {shared.pincount} pins' if shared.category != 'ferrule' else ''
conn_color = f', {shared.color}' if shared.color else ''
name = f'Connector{conn_type}{conn_subtype}{conn_pincount}{conn_color}'
item = {'item': name, 'qty': len(designators), 'unit': '',
'designators': designators if shared.category != 'ferrule' else ''}
bom_connectors.append(item)
bom_connectors = sorted(bom_connectors, key=lambda k: k['item']) # https://stackoverflow.com/a/73050
bom.extend(bom_connectors)
# cables
types = Counter([(v.category, v.gauge, v.gauge_unit, v.wirecount, v.shield) for v in self.cables.values()])
for maintype in types:
items = {k: v for k, v in self.cables.items() if (
v.category, v.gauge, v.gauge_unit, v.wirecount, v.shield) == maintype}
shared = next(iter(items.values()))
if shared.category != 'bundle':
designators = list(items.keys())
designators.sort()
total_length = sum(i.length for i in items.values())
gauge_name = f' x {shared.gauge} {shared.gauge_unit}'if shared.gauge else ' wires'
shield_name = ' shielded' if shared.shield else ''
name = f'Cable, {shared.wirecount}{gauge_name}{shield_name}'
item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators}
bom_cables.append(item)
# bundles (ignores wirecount)
wirelist = []
# list all cables again, since bundles are represented as wires internally, with the category='bundle' set
types = Counter([(v.category, v.gauge, v.gauge_unit, v.length) for v in self.cables.values()])
for maintype in types:
items = {k: v for k, v in self.cables.items() if (v.category, v.gauge, v.gauge_unit, v.length) == maintype}
shared = next(iter(items.values()))
# filter out cables that are not bundles
if shared.category == 'bundle':
for bundle in items.values():
# add each wire from each bundle to the wirelist
for color in bundle.colors:
wirelist.append({'gauge': shared.gauge, 'gauge_unit': shared.gauge_unit,
'length': shared.length, 'color': color, 'designator': bundle.name})
# join similar wires from all the bundles to a single BOM item
types = Counter([(v['gauge'], v['gauge_unit'], v['color']) for v in wirelist])
for maintype in types:
items = [v for v in wirelist if (v['gauge'], v['gauge_unit'], v['color']) == maintype]
shared = items[0]
designators = [i['designator'] for i in items]
# remove duplicates
designators = list(dict.fromkeys(designators))
designators.sort()
total_length = sum(i['length'] for i in items)
gauge_name = f', {shared["gauge"]} {shared["gauge_unit"]}' if shared['gauge'] else ''
gauge_color = f', {shared["color"]}' if shared['color'] != '' else ''
name = f'Wire{gauge_name}{gauge_color}'
item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators}
bom_cables.append(item)
bom_cables = sorted(bom_cables, key=lambda k: k['item']) # https://stackoverflow.com/a/73050
bom.extend(bom_cables)
return bom
def bom_list(self):
bom = self.bom()
keys = ['item', 'qty', 'unit', 'designators']
bom_list = []
bom_list.append([k.capitalize() for k in keys]) # create header row with keys
for item in bom:
item_list = [item.get(key, '') for key in keys] # fill missing values with blanks
for i, subitem in enumerate(item_list):
if isinstance(subitem, List): # convert any lists into comma separated strings
item_list[i] = ', '.join(subitem)
bom_list.append(item_list)
return bom_list

View File

@ -1,450 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse import argparse
from collections import Counter
from dataclasses import dataclass, field
from graphviz import Graph
import os import os
import sys import sys
from typing import Any, List
import yaml import yaml
if __name__== '__main__': if __name__ == '__main__':
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from wireviz import wv_colors from wireviz.Harness import Harness
from wireviz.wv_helper import nested, int2tuple, awg_equiv, flatten2d, tuplelist2tsv
class Harness:
def __init__(self):
self.color_mode = 'SHORT'
self.connectors = {}
self.cables = {}
def add_connector(self, name, *args, **kwargs):
self.connectors[name] = Connector(name, *args, **kwargs)
def add_cable(self, name, *args, **kwargs):
self.cables[name] = Cable(name, *args, **kwargs)
def loop(self, connector_name, from_pin, to_pin):
self.connectors[connector_name].loop(from_pin, to_pin)
def connect(self, from_name, from_pin, via_name, via_pin, to_name, to_pin):
self.cables[via_name].connect(from_name, from_pin, via_pin, to_name, to_pin)
if from_name in self.connectors:
self.connectors[from_name].activate_pin(from_pin)
if to_name in self.connectors:
self.connectors[to_name].activate_pin(to_pin)
def create_graph(self):
dot = Graph()
dot.body.append('// Graph generated by WireViz')
dot.body.append('// https://github.com/formatc1702/WireViz')
font = 'arial'
dot.attr('graph', rankdir='LR',
ranksep='2',
bgcolor='white',
nodesep='0.33',
fontname=font)
dot.attr('node', shape='record',
style='filled',
fillcolor='white',
fontname=font)
dot.attr('edge', style='bold',
fontname=font)
# prepare ports on connectors depending on which side they will connect
for k, c in self.cables.items():
for x in c.connections:
if x.from_port is not None: # connect to left
self.connectors[x.from_name].ports_right = True
if x.to_port is not None: # connect to right
self.connectors[x.to_name].ports_left = True
for k, n in self.connectors.items():
if n.category == 'ferrule':
infostring = '{type}{subtype} {color}'.format(type=n.type,
subtype=', {}'.format(n.subtype) if n.subtype else '',
color=wv_colors.translate_color(n.color, self.color_mode) if n.color else '')
infostring_l = infostring if n.ports_right else ''
infostring_r = infostring if n.ports_left else ''
dot.node(k, shape='none',
style='filled',
margin='0',
orientation = '0' if n.ports_left else '180',
label='''<
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="2"><TR>
<TD PORT="p1l"> {infostring_l} </TD>
{colorbar}
<TD PORT="p1r"> {infostring_r} </TD>
</TR></TABLE>
>'''.format(infostring_l=infostring_l,
infostring_r=infostring_r,
colorbar='<TD BGCOLOR="{}" BORDER="1" SIDES="LR" WIDTH="4"></TD>'.format(wv_colors.translate_color(n.color, 'HEX')) if n.color else ''))
else: # not a ferrule
# a = attributes
a = [n.type,
n.subtype,
'{}-pin'.format(n.pincount) if n.show_pincount else '']
# p = pinout
p = [[],[],[]]
for pinnumber, pinname in zip(n.pinnumbers, n.pinout):
if n.hide_disconnected_pins and not n.visible_pins.get(pinnumber, False):
continue
p[1].append(pinname)
if n.ports_left:
p[0].append('<p{portno}l>{portno}'.format(portno=pinnumber))
if n.ports_right:
p[2].append('<p{portno}r>{portno}'.format(portno=pinnumber))
# l = label
l = [n.name if n.show_name else '', a, p, n.notes]
dot.node(k, label=nested(l))
if len(n.loops) > 0:
dot.attr('edge',color='#000000:#ffffff:#000000')
if n.ports_left:
loop_side = 'l'
loop_dir = 'w'
elif n.ports_right:
loop_side = 'r'
loop_dir = 'e'
else:
raise Exception('No side for loops')
for loop in n.loops:
dot.edge('{name}:p{port_from}{loop_side}:{loop_dir}'.format(name=n.name, port_from=loop[0], port_to=loop[1], loop_side=loop_side, loop_dir=loop_dir),
'{name}:p{port_to}{loop_side}:{loop_dir}'.format(name=n.name, port_from=loop[0], port_to=loop[1], loop_side=loop_side, loop_dir=loop_dir))
for k, c in self.cables.items():
# a = attributes
a = ['{}x'.format(len(c.colors)) if c.show_wirecount else '',
'{} {}{}'.format(c.gauge, c.gauge_unit, ' ({} AWG)'.format(awg_equiv(c.gauge)) if c.gauge_unit == 'mm\u00B2' and c.show_equiv else '') if c.gauge else '', # TODO: show equiv
'+ S' if c.shield else '',
'{} m'.format(c.length) if c.length > 0 else '']
a = list(filter(None, a))
html = '<table border="0" cellspacing="0" cellpadding="0"><tr><td>' # main table
html = html + '<table border="0" cellspacing="0" cellpadding="3" cellborder="1">' # name+attributes table
if c.show_name:
html = html + '<tr><td colspan="{colspan}">{name}</td></tr>'.format(colspan=len(a), name=c.name)
html = html + '<tr>' # attribute row
for attrib in a:
html = html + '<td>{attrib}</td>'.format(attrib=attrib)
html = html + '</tr>' # attribute row
html = html + '</table></td></tr>' # name+attributes table
html = html + '<tr><td>&nbsp;</td></tr>' # spacer between attributes and wires
html = html + '<tr><td><table border="0" cellspacing="0" cellborder="0">' # conductor table
for i, x in enumerate(c.colors,1):
p = []
p.append('<!-- {}_in -->'.format(i))
p.append(wv_colors.translate_color(x, self.color_mode))
p.append('<!-- {}_out -->'.format(i))
html = html + '<tr>'
for bla in p:
html = html + '<td>{}</td>'.format(bla)
html = html + '</tr>'
bgcolor = wv_colors.translate_color(x, 'hex')
html = html + '<tr><td colspan="{colspan}" cellpadding="0" height="6" bgcolor="{bgcolor}" border="2" sides="tb" port="{port}"></td></tr>'.format(colspan=len(p), bgcolor=bgcolor if bgcolor != '' else '#ffffff', port='w{}'.format(i))
if c.shield:
p = ['<!-- s_in -->', 'Shield', '<!-- s_out -->']
html = html + '<tr><td>&nbsp;</td></tr>' # spacer
html = html + '<tr>'
for bla in p:
html = html + '<td>{}</td>'.format(bla)
html = html + '</tr>'
html = html + '<tr><td colspan="{colspan}" cellpadding="0" height="6" border="2" sides="b" port="{port}"></td></tr>'.format(colspan=len(p), bgcolor=wv_colors.translate_color(x, 'hex'), port='ws')
html = html + '<tr><td>&nbsp;</td></tr>' # spacer at the end
html = html + '</table>' # conductor table
html = html + '</td></tr>' # main table
if c.notes:
html = html + '<tr><td cellpadding="3">{}</td></tr>'.format(c.notes) # notes table
html = html + '<tr><td>&nbsp;</td></tr>' # spacer at the end
html = html + '</table>' # main table
# connections
for x in c.connections:
if isinstance(x.via_port, int): # check if it's an actual wire and not a shield
search_color = c.colors[x.via_port-1]
if search_color in wv_colors.color_hex:
dot.attr('edge',color='#000000:{wire_color}:#000000'.format(wire_color=wv_colors.color_hex[search_color]))
else: # color name not found
dot.attr('edge',color='#000000:#ffffff:#000000')
else: # it's a shield connection
dot.attr('edge',color='#000000')
if x.from_port is not None: # connect to left
from_ferrule = self.connectors[x.from_name].category == 'ferrule'
code_left_1 = '{from_name}{from_port}:e'.format(from_name=x.from_name, from_port=':p{}r'.format(x.from_port) if not from_ferrule else '')
code_left_2 = '{via_name}:w{via_wire}:w'.format(via_name=c.name, via_wire=x.via_port, via_subport='i' if c.show_pinout else '')
dot.edge(code_left_1, code_left_2)
from_string = '{}:{}'.format(x.from_name, x.from_port) if not from_ferrule else ''
html = html.replace('<!-- {}_in -->'.format(x.via_port), from_string)
if x.to_port is not None: # connect to right
to_ferrule = self.connectors[x.to_name].category == 'ferrule'
code_right_1 = '{via_name}:w{via_wire}:e'.format(via_name=c.name, via_wire=x.via_port, via_subport='o' if c.show_pinout else '')
code_right_2 = '{to_name}{to_port}:w'.format(to_name=x.to_name, to_port=':p{}l'.format(x.to_port) if not to_ferrule else '')
dot.edge(code_right_1, code_right_2)
to_string = '{}:{}'.format(x.to_name, x.to_port) if not to_ferrule else ''
html = html.replace('<!-- {}_out -->'.format(x.via_port), to_string)
dot.node(c.name, label='<{html}>'.format(html=html), shape='box', style='filled,dashed' if c.category=='bundle' else '', margin='0', fillcolor='white')
return dot
def output(self, filename, directory='_output', view=False, cleanup=True, format='pdf', gen_bom=False):
# graphical output
d = self.create_graph()
for f in format:
d.format = f
d.render(filename=filename, directory=directory, view=view, cleanup=cleanup)
d.save(filename='{}.gv'.format(filename), directory=directory)
# bom output
bom_list = self.bom_list()
with open('{}.bom.tsv'.format(filename),'w') as file:
file.write(tuplelist2tsv(bom_list))
# HTML output
with open('{}.html'.format(filename),'w') as file:
file.write('<html><body style="font-family:Arial">')
file.write('<h1>Diagram</h1>')
with open('{}.svg'.format(filename),'r') as svg:
for l in svg:
file.write(l)
file.write('<h1>Bill of Materials</h1>')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">')
file.write('<tr>')
for item in listy[0]:
file.write('<th align="left" style="border:1px solid #000000; padding: 8px">{}</th>'.format(item))
file.write('</tr>')
for row in listy[1:]:
file.write('<tr>')
for i, item in enumerate(row):
file.write('<td {align} style="border:1px solid #000000; padding: 4px">{content}</td>'.format(content=item, align='align="right"' if listy[0][i] == 'Qty' else ''))
file.write('</tr>')
file.write('</table>')
file.write('</body></html>')
def bom(self):
bom = []
bom_connectors = []
bom_cables = []
# connectors
types = Counter([(v.type, v.subtype, v.pincount) for v in self.connectors.values()])
for type in types:
items = {k: v for k, v in self.connectors.items() if (v.type, v.subtype, v.pincount) == type}
shared = next(iter(items.values()))
designators = list(items.keys())
designators.sort()
name = 'Connector{type}{subtype}{pincount}{color}'.format(type = ', {}'.format(shared.type) if shared.type else '',
subtype = ', {}'.format(shared.subtype) if shared.subtype else '',
pincount = ', {} pins'.format(shared.pincount) if shared.category != 'ferrule' else '',
color = ', {}'.format(shared.color) if shared.color else '')
item = {'item': name, 'qty': len(designators), 'unit': '', 'designators': designators if shared.category != 'ferrule' else ''}
bom_connectors.append(item)
bom_connectors = sorted(bom_connectors, key=lambda k: k['item']) # https://stackoverflow.com/a/73050
bom.extend(bom_connectors)
# cables
types = Counter([(v.category, v.gauge, v.gauge_unit, v.wirecount, v.shield) for v in self.cables.values()])
for type in types:
items = {k: v for k, v in self.cables.items() if (v.category, v.gauge, v.gauge_unit, v.wirecount, v.shield) == type}
shared = next(iter(items.values()))
if shared.category != 'bundle':
designators = list(items.keys())
designators.sort()
total_length = sum(i.length for i in items.values())
name = 'Cable, {wirecount}{gauge}{shield}'.format(wirecount = shared.wirecount,
gauge = ' x {} {}'.format(shared.gauge, shared.gauge_unit) if shared.gauge else ' wires',
shield = ' shielded' if shared.shield else '')
item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators}
bom_cables.append(item)
# bundles (ignores wirecount)
wirelist = []
# list all cables again, since bundles are represented as wires internally, with the category='bundle' set
types = Counter([(v.category, v.gauge, v.gauge_unit, v.length) for v in self.cables.values()])
for type in types:
items = {k: v for k, v in self.cables.items() if (v.category, v.gauge, v.gauge_unit, v.length) == type}
shared = next(iter(items.values()))
# filter out cables that are not bundles
if shared.category == 'bundle':
for bundle in items.values():
# add each wire from each bundle to the wirelist
for color in bundle.colors:
wirelist.append({'gauge': shared.gauge, 'gauge_unit': shared.gauge_unit, 'length': shared.length, 'color': color, 'designator': bundle.name})
# join similar wires from all the bundles to a single BOM item
types = Counter([(v['gauge'], v['gauge_unit'], v['color']) for v in wirelist])
for type in types:
items = [v for v in wirelist if (v['gauge'], v['gauge_unit'], v['color']) == type]
shared = items[0]
designators = [i['designator'] for i in items]
# remove duplicates
designators = list(dict.fromkeys(designators))
designators.sort()
total_length = sum(i['length'] for i in items)
name = 'Wire{gauge}{color}'.format(gauge=', {} {}'.format(shared['gauge'], shared['gauge_unit']) if shared['gauge'] else '',
color=', {}'.format(shared['color']) if shared['color'] != '' else '')
item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators}
bom_cables.append(item)
bom_cables = sorted(bom_cables, key=lambda k: k['item']) # https://stackoverflow.com/a/73050
bom.extend(bom_cables)
return bom
def bom_list(self):
bom = self.bom()
keys = ['item', 'qty', 'unit', 'designators']
bom_list = []
bom_list.append([k.capitalize() for k in keys]) # create header row with keys
for item in bom:
item_list = [item.get(key, '') for key in keys] # fill missing values with blanks
for i, subitem in enumerate(item_list):
if isinstance(subitem, List): # convert any lists into comma separated strings
item_list[i] = ', '.join(subitem)
bom_list.append(item_list)
return bom_list
@dataclass
class Connector:
name: str
category: str = None
type: str = None
subtype: str = None
pincount: int = None
notes: str = None
pinout: List[Any] = field(default_factory=list)
pinnumbers: List[Any] = field(default_factory=list)
color: str = None
show_name: bool = True
show_pincount: bool = True
hide_disconnected_pins: bool = False
def __post_init__(self):
self.ports_left = False
self.ports_right = False
self.loops = []
self.visible_pins = {}
if self.pincount is None:
if self.pinout:
self.pincount = len(self.pinout)
elif self.pinnumbers:
self.pincount = len(self.pinnumbers)
elif self.category == 'ferrule':
self.pincount = 1
else:
raise Exception('You need to specify at least one, pincount, pinout or pinnumbers')
if self.pinout and self.pinnumbers:
if len(self.pinout) != len(self.pinnumbers):
raise Exception('Given pinout and pinnumbers size mismatch')
# create default lists for pinnumbers (sequential) and pinouts (blank) if not specified
if not self.pinnumbers:
self.pinnumbers = list(range(1,self.pincount + 1))
if not self.pinout:
self.pinout = [''] * self.pincount
def loop(self, from_pin, to_pin):
self.loops.append((from_pin, to_pin))
if self.hide_disconnected_pins:
self.visible_pins[from_pin] = True
self.visible_pins[to_pin] = True
def activate_pin(self, pin):
self.visible_pins[pin] = True
@dataclass
class Cable:
name: str
category : str = None
type: str = None
gauge: float = None
gauge_unit : str = None
show_equiv: bool = False
length: float = 0
wirecount: int = None
shield: bool = False
notes: str = None
colors: List[Any] = field(default_factory=list)
color_code: str = None
show_name: bool = True
show_pinout: bool = False
show_wirecount: bool = True
def __post_init__(self):
if isinstance(self.gauge, str): # gauge and unit specified
try:
g, u = self.gauge.split(' ')
except:
raise Exception('Gauge must be a number, or number and unit separated by a space')
self.gauge = g
self.gauge_unit = u.replace('mm2','mm\u00B2')
elif self.gauge is not None: # gauge specified, assume mm2
if self.gauge_unit is None:
self.gauge_unit = 'mm\u00B2'
else:
pass # gauge not specified
self.connections = []
if self.wirecount: # number of wires explicitly defined
if self.colors: # use custom color palette (partly or looped if needed)
pass
elif self.color_code: # use standard color palette (partly or looped if needed)
if self.color_code not in wv_colors.COLOR_CODES:
raise Exception('Unknown color code')
self.colors = wv_colors.COLOR_CODES[self.color_code]
else: # no colors defined, add dummy colors
self.colors = [''] * self.wirecount
# make color code loop around if more wires than colors
if self.wirecount > len(self.colors):
m = self.wirecount // len(self.colors) + 1
self.colors = self.colors * int(m)
# cut off excess after looping
self.colors = self.colors[:self.wirecount]
else: # wirecount implicit in length of color list
if not self.colors:
raise Exception('Unknown number of wires. Must specify wirecount or colors (implicit length)')
self.wirecount = len(self.colors)
# for BOM generation
self.wirecount_and_shield = (self.wirecount, self.shield)
def connect(self, from_name, from_pin, via_pin, to_name, to_pin):
from_pin = int2tuple(from_pin)
via_pin = int2tuple(via_pin)
to_pin = int2tuple(to_pin)
if len(from_pin) != len(to_pin):
raise Exception('from_pin must have the same number of elements as to_pin')
for i, x in enumerate(from_pin):
# self.connections.append((from_name, from_pin[i], via_pin[i], to_name, to_pin[i]))
self.connections.append(Connection(from_name, from_pin[i], via_pin[i], to_name, to_pin[i]))
@dataclass
class Connection:
from_name: Any
from_port: Any
via_port: Any
to_name: Any
to_port: Any
def parse(yaml_input, file_out=None, generate_bom=False): def parse(yaml_input, file_out=None, generate_bom=False):
@ -457,23 +25,23 @@ def parse(yaml_input, file_out=None, generate_bom=False):
# if str is of the format '#-#', it is treated as a range (inclusive) and expanded # if str is of the format '#-#', it is treated as a range (inclusive) and expanded
output = [] output = []
if not isinstance(yaml_data, list): if not isinstance(yaml_data, list):
yaml_data = [yaml_data,] yaml_data = [yaml_data]
for e in yaml_data: for e in yaml_data:
e = str(e) e = str(e)
if '-' in e: # list of pins if '-' in e: # list of pins
a, b = tuple(map(int, e.split('-'))) a, b = tuple(map(int, e.split('-')))
if a < b: if a < b:
for x in range(a,b+1): for x in range(a, b + 1):
output.append(x) output.append(x)
elif a > b: elif a > b:
for x in range(a,b-1,-1): for x in range(a, b - 1, -1):
output.append(x) output.append(x)
elif a == b: elif a == b:
output.append(a) output.append(a)
else: else:
try: try:
x = int(e) x = int(e)
except: except Exception:
x = e x = e
output.append(x) output.append(x)
return output return output
@ -484,25 +52,25 @@ def parse(yaml_input, file_out=None, generate_bom=False):
return False return False
return True return True
h = Harness() harness = Harness()
# add items # add items
sections = ['connectors','cables','ferrules','connections'] sections = ['connectors', 'cables', 'ferrules', 'connections']
types = [dict, dict, dict, list] types = [dict, dict, dict, list]
for sec, ty in zip(sections, types): for sec, ty in zip(sections, types):
if sec in yaml_data and type(yaml_data[sec]) == ty: if sec in yaml_data and type(yaml_data[sec]) == ty:
if len(yaml_data[sec]) > 0: if len(yaml_data[sec]) > 0:
if ty == dict: if ty == dict:
for k, o in yaml_data[sec].items(): for key, o in yaml_data[sec].items():
if sec == 'connectors': if sec == 'connectors':
h.add_connector(name=k, **o) harness.add_connector(name=key, **o)
elif sec == 'cables': elif sec == 'cables':
h.add_cable(name=k, **o) harness.add_cable(name=key, **o)
elif sec == 'ferrules': elif sec == 'ferrules':
pass pass
else: else:
pass # section exists but is empty pass # section exists but is empty
else: # section does not exist, create empty section else: # section does not exist, create empty section
if ty == dict: if ty == dict:
yaml_data[sec] = {} yaml_data[sec] = {}
elif ty == list: elif ty == list:
@ -510,64 +78,63 @@ def parse(yaml_input, file_out=None, generate_bom=False):
# add connections # add connections
ferrule_counter = 0 ferrule_counter = 0
for con in yaml_data['connections']: for connections in yaml_data['connections']:
if len(con) == 3: # format: connector -- cable -- connector if len(connections) == 3: # format: connector -- cable -- connector
for c in con: for connection in connections:
if len(list(c.keys())) != 1: # check that each entry in con has only one key, which is the designator if len(list(connection.keys())) != 1: # check that each entry in con has only one key, which is the designator
raise Exception('Too many keys') raise Exception('Too many keys')
from_name = list(con[0].keys())[0] from_name = list(connections[0].keys())[0]
via_name = list(con[1].keys())[0] via_name = list(connections[1].keys())[0]
to_name = list(con[2].keys())[0] to_name = list(connections[2].keys())[0]
if not check_designators([from_name,via_name,to_name],('connectors','cables','connectors')): if not check_designators([from_name, via_name, to_name], ('connectors', 'cables', 'connectors')):
print([from_name,via_name,to_name]) print([from_name, via_name, to_name])
raise Exception('Bad connection definition (3)') raise Exception('Bad connection definition (3)')
from_pins = expand(con[0][from_name]) from_pins = expand(connections[0][from_name])
via_pins = expand(con[1][via_name]) via_pins = expand(connections[1][via_name])
to_pins = expand(con[2][to_name]) to_pins = expand(connections[2][to_name])
if len(from_pins) != len(via_pins) or len(via_pins) != len(to_pins): if len(from_pins) != len(via_pins) or len(via_pins) != len(to_pins):
raise Exception('List length mismatch') raise Exception('List length mismatch')
for (from_pin, via_pin, to_pin) in zip(from_pins, via_pins, to_pins): for (from_pin, via_pin, to_pin) in zip(from_pins, via_pins, to_pins):
h.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin)
elif len(con) == 2: elif len(connections) == 2:
for c in con: for connection in connections:
if type(c) is dict: if type(connection) is dict:
if len(list(c.keys())) != 1: # check that each entry in con has only one key, which is the designator if len(list(connection.keys())) != 1: # check that each entry in con has only one key, which is the designator
raise Exception('Too many keys') raise Exception('Too many keys')
# hack to make the format for ferrules compatible with the formats for connectors and cables # hack to make the format for ferrules compatible with the formats for connectors and cables
if type(con[0]) == str: if type(connections[0]) == str:
name = con[0] name = connections[0]
con[0] = {} connections[0] = {}
con[0][name] = name connections[0][name] = name
if type(con[1]) == str: if type(connections[1]) == str:
name = con[1] name = connections[1]
con[1] = {} connections[1] = {}
con[1][name] = name connections[1][name] = name
from_name = list(con[0].keys())[0] from_name = list(connections[0].keys())[0]
to_name = list(con[1].keys())[0] to_name = list(connections[1].keys())[0]
con_cbl = check_designators([from_name, to_name],('connectors','cables')) con_cbl = check_designators([from_name, to_name], ('connectors', 'cables'))
cbl_con = check_designators([from_name, to_name],('cables','connectors')) cbl_con = check_designators([from_name, to_name], ('cables', 'connectors'))
con_con = check_designators([from_name, to_name],('connectors','connectors')) con_con = check_designators([from_name, to_name], ('connectors', 'connectors'))
fer_cbl = check_designators([from_name, to_name], ('ferrules', 'cables'))
fer_cbl = check_designators([from_name, to_name],('ferrules','cables')) cbl_fer = check_designators([from_name, to_name], ('cables', 'ferrules'))
cbl_fer = check_designators([from_name, to_name],('cables','ferrules'))
if not con_cbl and not cbl_con and not con_con and not fer_cbl and not cbl_fer: if not con_cbl and not cbl_con and not con_con and not fer_cbl and not cbl_fer:
raise Exception('Wrong designators') raise Exception('Wrong designators')
from_pins = expand(con[0][from_name]) from_pins = expand(connections[0][from_name])
to_pins = expand(con[1][to_name]) to_pins = expand(connections[1][to_name])
if con_cbl or cbl_con or con_con: if con_cbl or cbl_con or con_con:
if len(from_pins) != len(to_pins): if len(from_pins) != len(to_pins):
@ -576,19 +143,19 @@ def parse(yaml_input, file_out=None, generate_bom=False):
if con_cbl or cbl_con: if con_cbl or cbl_con:
for (from_pin, to_pin) in zip(from_pins, to_pins): for (from_pin, to_pin) in zip(from_pins, to_pins):
if con_cbl: if con_cbl:
h.connect(from_name, from_pin, to_name, to_pin, None, None) harness.connect(from_name, from_pin, to_name, to_pin, None, None)
else: # cbl_con else: # cbl_con
h.connect(None, None, from_name, from_pin, to_name, to_pin) harness.connect(None, None, from_name, from_pin, to_name, to_pin)
elif con_con: elif con_con:
cocon_coname = list(con[0].keys())[0] cocon_coname = list(connections[0].keys())[0]
from_pins = expand(con[0][from_name]) from_pins = expand(connections[0][from_name])
to_pins = expand(con[1][to_name]) to_pins = expand(connections[1][to_name])
for (from_pin, to_pin) in zip(from_pins, to_pins): for (from_pin, to_pin) in zip(from_pins, to_pins):
h.loop(cocon_coname, from_pin, to_pin) harness.loop(cocon_coname, from_pin, to_pin)
if fer_cbl or cbl_fer: if fer_cbl or cbl_fer:
from_pins = expand(con[0][from_name]) from_pins = expand(connections[0][from_name])
to_pins = expand(con[1][to_name]) to_pins = expand(connections[1][to_name])
if fer_cbl: if fer_cbl:
ferrule_name = from_name ferrule_name = from_name
@ -602,19 +169,19 @@ def parse(yaml_input, file_out=None, generate_bom=False):
ferrule_params = yaml_data['ferrules'][ferrule_name] ferrule_params = yaml_data['ferrules'][ferrule_name]
for cable_pin in cable_pins: for cable_pin in cable_pins:
ferrule_counter = ferrule_counter + 1 ferrule_counter = ferrule_counter + 1
ferrule_id = '_F{}'.format(ferrule_counter) ferrule_id = f'_F{ferrule_counter}'
h.add_connector(ferrule_id, category='ferrule', **ferrule_params) harness.add_connector(ferrule_id, category='ferrule', **ferrule_params)
if fer_cbl: if fer_cbl:
h.connect(ferrule_id, 1, cable_name, cable_pin, None, None) harness.connect(ferrule_id, 1, cable_name, cable_pin, None, None)
else: else:
h.connect(None, None, cable_name, cable_pin, ferrule_id, 1) harness.connect(None, None, cable_name, cable_pin, ferrule_id, 1)
else: else:
raise Exception('Wrong number of connection parameters') raise Exception('Wrong number of connection parameters')
h.output(filename=file_out, format=('png','svg'), gen_bom=generate_bom, view=False) harness.output(filename=file_out, fmt=('png', 'svg'), gen_bom=generate_bom, view=False)
def parse_file(yaml_file, file_out=None, generate_bom=False): def parse_file(yaml_file, file_out=None, generate_bom=False):
with open(yaml_file, 'r') as file: with open(yaml_file, 'r') as file:
@ -630,27 +197,26 @@ def parse_file(yaml_file, file_out=None, generate_bom=False):
def parse_cmdline(): def parse_cmdline():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Generate cable and wiring harness documentation from YAML descriptions' description='Generate cable and wiring harness documentation from YAML descriptions',
) )
parser.add_argument('input_file', action='store', type=str, metavar='YAML_FILE') parser.add_argument('input_file', action='store', type=str, metavar='YAML_FILE')
parser.add_argument('-o', '--output_file', action='store', type=str, metavar='OUTPUT') parser.add_argument('-o', '--output_file', action='store', type=str, metavar='OUTPUT')
parser.add_argument('--generate-bom', action='store_true', default=True) parser.add_argument('--generate-bom', action='store_true', default=True)
parser.add_argument('--prepend-file', action='store', type=str, metavar='YAML_FILE') parser.add_argument('--prepend-file', action='store', type=str, metavar='YAML_FILE')
args = parser.parse_args() return parser.parse_args()
return args
def main(): def main():
args = parse_cmdline() args = parse_cmdline()
if not os.path.exists(args.input_file): if not os.path.exists(args.input_file):
print('Error: input file {} inaccessible or does not exist, check path'.format(args.input_file)) print(f'Error: input file {args.input_file} inaccessible or does not exist, check path')
sys.exit(1) sys.exit(1)
with open(args.input_file) as fh: with open(args.input_file) as fh:
@ -658,7 +224,7 @@ def main():
if args.prepend_file: if args.prepend_file:
if not os.path.exists(args.prepend_file): if not os.path.exists(args.prepend_file):
print('Error: prepend input file {} inaccessible or does not exist, check path'.format(args.prepend_file)) print(f'Error: prepend input file {args.prepend_file} inaccessible or does not exist, check path')
sys.exit(1) sys.exit(1)
with open(args.prepend_file) as fh: with open(args.prepend_file) as fh:
prepend = fh.read() prepend = fh.read()
@ -667,12 +233,13 @@ def main():
if not args.output_file: if not args.output_file:
file_out = args.input_file file_out = args.input_file
pre, _ = os.path.splitext(file_out) pre, _ = os.path.splitext(file_out)
file_out = pre # extension will be added by graphviz output function file_out = pre # extension will be added by graphviz output function
else: else:
file_out = args.output_file file_out = args.output_file
file_out = os.path.abspath(file_out) file_out = os.path.abspath(file_out)
parse(yaml_input, file_out=file_out, generate_bom=args.generate_bom) parse(yaml_input, file_out=file_out, generate_bom=args.generate_bom)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,74 +1,77 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
COLOR_CODES = { COLOR_CODES = {
'DIN': ['WH','BN','GN','YE','GY','PK','BU','RD','BK','VT'], # ,'GYPK','RDBU','WHGN','BNGN','WHYE','YEBN','WHGY','GYBN','WHPK','PKBN'], 'DIN': ['WH', 'BN', 'GN', 'YE', 'GY', 'PK', 'BU', 'RD', 'BK', 'VT'], # ,'GYPK','RDBU','WHGN','BNGN','WHYE','YEBN','WHGY','GYBN','WHPK','PKBN'],
'IEC': ['BN','RD','OG','YE','GN','BU','VT','GY','WH','BK'], 'IEC': ['BN', 'RD', 'OG', 'YE', 'GN', 'BU', 'VT', 'GY', 'WH', 'BK'],
'BW': ['BK','WH'] 'BW': ['BK', 'WH'],
} }
color_hex = { color_hex = {
'BK': '#000000', 'BK': '#000000',
'WH': '#ffffff', 'WH': '#ffffff',
'GY': '#999999', 'GY': '#999999',
'PK': '#ff66cc', 'PK': '#ff66cc',
'RD': '#ff0000', 'RD': '#ff0000',
'OG': '#ff8000', 'OG': '#ff8000',
'YE': '#ffff00', 'YE': '#ffff00',
'GN': '#00ff00', 'GN': '#00ff00',
'TQ': '#00ffff', 'TQ': '#00ffff',
'BU': '#0066ff', 'BU': '#0066ff',
'VT': '#8000ff', 'VT': '#8000ff',
'BN': '#666600', 'BN': '#666600',
} }
color_full = { color_full = {
'BK': 'black', 'BK': 'black',
'WH': 'white', 'WH': 'white',
'GY': 'grey', 'GY': 'grey',
'PK': 'pink', 'PK': 'pink',
'RD': 'red', 'RD': 'red',
'OG': 'orange', 'OG': 'orange',
'YE': 'yellow', 'YE': 'yellow',
'GN': 'green', 'GN': 'green',
'TQ': 'turquoise', 'TQ': 'turquoise',
'BU': 'blue', 'BU': 'blue',
'VT': 'violet', 'VT': 'violet',
'BN': 'brown', 'BN': 'brown',
} }
color_ger = { color_ger = {
'BK': 'sw', 'BK': 'sw',
'WH': 'ws', 'WH': 'ws',
'GY': 'gr', 'GY': 'gr',
'PK': 'rs', 'PK': 'rs',
'RD': 'rt', 'RD': 'rt',
'OG': 'or', 'OG': 'or',
'YE': 'ge', 'YE': 'ge',
'GN': 'gn', 'GN': 'gn',
'TQ': 'tk', 'TQ': 'tk',
'BU': 'bl', 'BU': 'bl',
'VT': 'vi', 'VT': 'vi',
'BN': 'br', 'BN': 'br',
} }
def translate_color(input, color_mode):
if input == '': def translate_color(inp, color_mode):
if inp == '':
output = '' output = ''
else: else:
if color_mode == 'full': if color_mode == 'full':
output = color_full[input].lower() output = color_full[inp].lower()
elif color_mode == 'FULL': elif color_mode == 'FULL':
output = color_full[input].upper() output = color_full[inp].upper()
elif color_mode == 'hex': elif color_mode == 'hex':
output = color_hex[input].lower() output = color_hex[inp].lower()
elif color_mode == 'HEX': elif color_mode == 'HEX':
output = color_hex[input].upper() output = color_hex[inp].upper()
elif color_mode == 'ger': elif color_mode == 'ger':
output = color_ger[input].lower() output = color_ger[inp].lower()
elif color_mode == 'GER': elif color_mode == 'GER':
output = color_ger[input].upper() output = color_ger[inp].upper()
elif color_mode == 'short': elif color_mode == 'short':
output = input.lower() output = inp.lower()
elif color_mode == 'SHORT': elif color_mode == 'SHORT':
output = input.upper() output = inp.upper()
else: else:
raise Exception('Unknown color mode') raise Exception('Unknown color mode')
return output return output

View File

@ -1,33 +1,37 @@
from typing import Any, List #!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import List
def awg_equiv(mm2): def awg_equiv(mm2):
awg_equiv_table = { awg_equiv_table = {
'0.09': 28, '0.09': 28,
'0.14': 26, '0.14': 26,
'0.25': 24, '0.25': 24,
'0.34': 22, '0.34': 22,
'0.5': 21, '0.5': 21,
'0.75': 20, '0.75': 20,
'1': 18, '1': 18,
'1.5': 16, '1.5': 16,
'2.5': 14, '2.5': 14,
'4': 12, '4': 12,
'6': 10, '6': 10,
'10': 8, '10': 8,
'16': 6, '16': 6,
'25': 4, '25': 4,
'35': 2, '35': 2,
'50': 1, '50': 1,
} }
k = str(mm2) k = str(mm2)
if k in awg_equiv_table: if k in awg_equiv_table:
return awg_equiv_table[k] return awg_equiv_table[k]
else: else:
return 'unknown' return 'unknown'
def nested(input):
def nested(inp):
l = [] l = []
for x in input: for x in inp:
if isinstance(x, list): if isinstance(x, list):
if len(x) > 0: if len(x) > 0:
n = nested(x) n = nested(x)
@ -37,25 +41,26 @@ def nested(input):
if x is not None: if x is not None:
if x != '': if x != '':
l.append(str(x)) l.append(str(x))
s = '|'.join(l) return '|'.join(l)
return s
def int2tuple(input):
if isinstance(input, tuple): def int2tuple(inp):
output = input if isinstance(inp, tuple):
output = inp
else: else:
output = (input,) output = (inp,)
return output return output
def flatten2d(input):
output = [[str(item) if not isinstance(item, List) else ', '.join(item) for item in row] for row in input]
return output
def tuplelist2tsv(input, header=None): def flatten2d(inp):
return [[str(item) if not isinstance(item, List) else ', '.join(item) for item in row] for row in inp]
def tuplelist2tsv(inp, header=None):
output = '' output = ''
if header is not None: if header is not None:
input.insert(0, header) inp.insert(0, header)
input = flatten2d(input) inp = flatten2d(inp)
for row in input: for row in inp:
output = output + '\t'.join(str(item) for item in row) + '\n' output = output + '\t'.join(str(item) for item in row) + '\n'
return output return output