diff --git a/.gitignore b/.gitignore index c121d2f..cc614ad 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ build data dist +venv/ diff --git a/README.md b/README.md index 3b90b58..b96dd42 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ WireViz is a tool for easily documenting cables, wiring harnesses and connector * [DIN 47100](https://en.wikipedia.org/wiki/DIN_47100) (WT/BN/GN/YE/GY/PK/BU/RD/BK/VT/...) * [IEC 62](https://en.wikipedia.org/wiki/Electronic_color_code#Color_band_system) (BN/RD/OR/YE/GN/BU/VT/GY/WT/BK/...) * Understands wire gauge in mm² or AWG - * Optionally auto-calculates and displays AWG equivalent when specifying mm² + * Optionally auto-calculates equivalent gauge between mm² and AWG * Allows more than one connector per side, as well as loopbacks * Allows for easy-autorouting for 1-to-1 wiring * Generates BOM (Bill of Materials) @@ -63,7 +63,7 @@ cables: connections: - - - X1: [5,2,1] + - X1: [5,2,3] - W1: [1,2,3] - X2: [1,3,2] - @@ -88,14 +88,6 @@ Output file: See the [tutorial page](tutorial/readme.md) for sample code, as well as the [example gallery](examples/readme.md) to see more of what WireViz can do. -### (Re-)Building the example projects - -If you would like to rebuild all of the included demos, examples and tutorials, use the ```build_examples.py``` script: - -```cd src/wireviz -./build_examples.py -``` - ## Usage ``` @@ -112,6 +104,13 @@ mywire.bom.tsv BOM (bill of materials) as tab-separated text file mywire.html HTML page with wiring diagram and BOM embedded ``` +### (Re-)Building the example projects + +If you would like to rebuild all of the included demos, examples and tutorials, use the ```build_examples.py``` script: + +```cd src/wireviz +./build_examples.py +``` ## Status This is very much a [work in progress](https://github.com/formatc1702/WireViz/projects/1). Source code, API, syntax and functionality may change wildly at any time. diff --git a/examples/demo01.gv b/examples/demo01.gv index 24abe51..09194a3 100644 --- a/examples/demo01.gv +++ b/examples/demo01.gv @@ -13,9 +13,9 @@ graph { X1:p2r:e -- W1:w2:w W1:w2:e -- X2:p3l:w edge [color="#000000:#00ff00:#000000"] - X1:p1r:e -- W1:w3:w + X1:p3r:e -- W1:w3:w W1:w3:e -- X2:p2l:w edge [color="#000000"] X1:p5r:e -- W1:ws:w - W1 [label=<
W1
3x0.25 mm²+ S0.2 m
 
X1:5WHX2:1
X1:2BNX2:3
X1:1GNX2:2
 
X1:5Shield
 
> fillcolor=white margin=0 shape=box style=""] + W1 [label=<
W1
3x0.25 mm²+ S0.2 m
 
X1:5WHX2:1
X1:2BNX2:3
X1:3GNX2:2
 
X1:5Shield
 
> fillcolor=white margin=0 shape=box style=""] } diff --git a/examples/demo01.html b/examples/demo01.html index cfedd06..d5231e0 100644 --- a/examples/demo01.html +++ b/examples/demo01.html @@ -83,7 +83,7 @@ -X1:1 +X1:3 GN X2:2 @@ -112,9 +112,9 @@ X1:e--W1:w - - - + + + diff --git a/examples/demo01.png b/examples/demo01.png index 6e56db1..77efff4 100644 Binary files a/examples/demo01.png and b/examples/demo01.png differ diff --git a/examples/demo01.svg b/examples/demo01.svg index bc7b2f9..517d58e 100644 --- a/examples/demo01.svg +++ b/examples/demo01.svg @@ -83,7 +83,7 @@ -X1:1 +X1:3 GN X2:2 @@ -112,9 +112,9 @@ X1:e--W1:w - - - + + + diff --git a/examples/demo01.yml b/examples/demo01.yml index b125176..ae1b4a5 100644 --- a/examples/demo01.yml +++ b/examples/demo01.yml @@ -18,7 +18,7 @@ cables: connections: - - - X1: [5,2,1] + - X1: [5,2,3] - W1: [1,2,3] - X2: [1,3,2] - diff --git a/examples/ex02.bom.tsv b/examples/ex02.bom.tsv index 4c8d11e..67cb7f0 100644 --- a/examples/ex02.bom.tsv +++ b/examples/ex02.bom.tsv @@ -1,4 +1,5 @@ Item Qty Unit Designators Connector, Molex Micro-Fit, female, 2 pins 3 X2, X3, X4 Connector, Molex Micro-Fit, male, 2 pins 1 X1 -Cable, 2 x 0.25 mm² 0.6 m W1, W2, W3 +Cable, 2 x 0.25 mm² 0.4 m W1, W2 +Cable, 2 x 20 AWG 0.2 m W3 diff --git a/examples/ex02.gv b/examples/ex02.gv index b914864..8f29aaf 100644 --- a/examples/ex02.gv +++ b/examples/ex02.gv @@ -28,5 +28,5 @@ graph { edge [color="#000000:#ff0000:#000000"] X1:p2r:e -- W3:w2:w W3:w2:e -- X4:p2l:w - W3 [label=<
W3
2x0.25 mm² (24 AWG)0.2 m
 
X1:1BKX4:1
X1:2RDX4:2
 
> fillcolor=white margin=0 shape=box style=""] + W3 [label=<
W3
2x20 AWG (0.75 mm²)0.2 m
 
X1:1BKX4:1
X1:2RDX4:2
 
> fillcolor=white margin=0 shape=box style=""] } diff --git a/examples/ex02.html b/examples/ex02.html index 148fa57..be3f0ab 100644 --- a/examples/ex02.html +++ b/examples/ex02.html @@ -119,7 +119,7 @@ 2x -0.25 mm² (24 AWG) +20 AWG (0.75 mm²) 0.2 m   @@ -255,4 +255,4 @@
-

Bill of Materials

ItemQtyUnitDesignators
Connector, Molex Micro-Fit, female, 2 pins3X2, X3, X4
Connector, Molex Micro-Fit, male, 2 pins1X1
Cable, 2 x 0.25 mm²0.6mW1, W2, W3
\ No newline at end of file +

Bill of Materials

ItemQtyUnitDesignators
Connector, Molex Micro-Fit, female, 2 pins3X2, X3, X4
Connector, Molex Micro-Fit, male, 2 pins1X1
Cable, 2 x 0.25 mm²0.4mW1, W2
Cable, 2 x 20 AWG0.2mW3
\ No newline at end of file diff --git a/examples/ex02.png b/examples/ex02.png index 7dbb395..28d54eb 100644 Binary files a/examples/ex02.png and b/examples/ex02.png differ diff --git a/examples/ex02.svg b/examples/ex02.svg index 6ddeb7b..5de828e 100644 --- a/examples/ex02.svg +++ b/examples/ex02.svg @@ -119,7 +119,7 @@ 2x -0.25 mm² (24 AWG) +20 AWG (0.75 mm²) 0.2 m   diff --git a/examples/ex02.yml b/examples/ex02.yml index 056e6fe..8a671e6 100644 --- a/examples/ex02.yml +++ b/examples/ex02.yml @@ -22,6 +22,7 @@ cables: <<: *wire_power # create from template W3: <<: *wire_power # create from template + gauge: 20 awg connections: - diff --git a/examples/ex08.gv b/examples/ex08.gv index a949333..2e189bb 100644 --- a/examples/ex08.gv +++ b/examples/ex08.gv @@ -4,7 +4,7 @@ graph { graph [bgcolor=white fontname=arial nodesep=0.33 rankdir=LR ranksep=2] node [fillcolor=white fontname=arial shape=record style=filled] edge [fontname=arial style=bold] - Key [label="Key|{Phone Connector|male 3.5|3-pin}|{{Dot|Dash|Ground}|{T|R|S}}"] + Key [label="Key|{Phone Connector|male 3.5}|{{Dot|Dash|Ground}|{T|R|S}}"] edge [color="#000000:#ffffff:#000000"] Key:pSr:e -- W1:w1:w edge [color="#000000:#666600:#000000"] diff --git a/examples/ex08.html b/examples/ex08.html index c290a0f..854d239 100644 --- a/examples/ex08.html +++ b/examples/ex08.html @@ -4,95 +4,93 @@ - + - + Key - -Key - + +Key + Phone Connector male 3.5 - -3-pin - -Dot - -Dash - -Ground - -T - -R - -S + +Dot + +Dash + +Ground + +T + +R + +S W1 - - -W1 - -3x - -24 AWG - -+ S - -0.2 m -  -Key:S -WH - - - -Key:R -BN - - - -Key:T -GN - - - -  -Key:S -Shield - -  + + +W1 + +3x + +24 AWG + ++ S + +0.2 m +  +Key:S +WH + + + +Key:R +BN + + + +Key:T +GN + + + +  +Key:S +Shield + +  Key:e--W1:w - - - + + + Key:e--W1:w - - - + + + Key:e--W1:w - - - + + + Key:e--W1:w - + diff --git a/examples/ex08.png b/examples/ex08.png index a0270f7..7767320 100644 Binary files a/examples/ex08.png and b/examples/ex08.png differ diff --git a/examples/ex08.svg b/examples/ex08.svg index 0599c11..630f541 100644 --- a/examples/ex08.svg +++ b/examples/ex08.svg @@ -4,95 +4,93 @@ - + - + Key - -Key - + +Key + Phone Connector male 3.5 - -3-pin - -Dot - -Dash - -Ground - -T - -R - -S + +Dot + +Dash + +Ground + +T + +R + +S W1 - - -W1 - -3x - -24 AWG - -+ S - -0.2 m -  -Key:S -WH - - - -Key:R -BN - - - -Key:T -GN - - - -  -Key:S -Shield - -  + + +W1 + +3x + +24 AWG + ++ S + +0.2 m +  +Key:S +WH + + + +Key:R +BN + + + +Key:T +GN + + + +  +Key:S +Shield + +  Key:e--W1:w - - - + + + Key:e--W1:w - - - + + + Key:e--W1:w - - - + + + Key:e--W1:w - + diff --git a/examples/ex08.yml b/examples/ex08.yml index 0d42be5..e65813a 100644 --- a/examples/ex08.yml +++ b/examples/ex08.yml @@ -6,6 +6,7 @@ connectors: subtype: male 3.5 pinnumbers: [T, R, S] pinout: [Dot, Dash, Ground] + show_pincount: false cables: W1: diff --git a/setup.py b/setup.py index 83dbc55..950e314 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( ], license='GPLv3', keywords='cable connector hardware harness wiring wiring-diagram wiring-harness', - url='https://github.com/n42/WireViz', + url='https://github.com/formatc1702/WireViz', package_dir={'': 'src'}, packages=find_packages('src'), entry_points={ diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py new file mode 100644 index 0000000..52ff983 --- /dev/null +++ b/src/wireviz/DataClasses.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Optional, List, Any, Union +from dataclasses import dataclass, field +from wireviz.wv_helper import int2tuple +from wireviz import wv_colors + + +@dataclass +class Connector: + name: str + manufacturer: Optional[str] = None + manufacturer_part_number: Optional[str] = None + internal_part_number: Optional[str] = None + 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 + manufacturer: Optional[Union[str, List[str]]] = None + manufacturer_part_number: Optional[Union[str, List[str]]] = None + internal_part_number: Optional[Union[str, List[str]]] = None + 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_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 + + if u.upper() == 'AWG': + self.gauge_unit = u.upper() + else: + 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) + + # if lists of part numbers are provided check this is a bundle and that it matches the wirecount. + for idfield in [self.manufacturer, self.manufacturer_part_number, self.internal_part_number]: + if isinstance(idfield, list): + if self.category == "bundle": + # check the length + if len(idfield) != self.wirecount: + raise Exception('lists of part data must match wirecount') + else: + raise Exception('lists of part data are only supported for bundles') + + # 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 diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py new file mode 100644 index 0000000..f33f012 --- /dev/null +++ b/src/wireviz/Harness.py @@ -0,0 +1,408 @@ +#!/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, mm2_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}' + + # id = identification + identification = [connector.manufacturer, + f'MPN: {connector.manufacturer_part_number}' if connector.manufacturer_part_number else '', + f'IPN: {connector.internal_part_number}' if connector.internal_part_number else ''] + identification = list(filter(None, identification)) + if(len(identification) > 0): + infostring = f'{infostring}
' + for attrib in identification: + infostring = f'{infostring}{attrib}, ' + infostring = infostring[:-2] # remove trainling comma and space + + 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'' if connector.color else '' + dot.node(key, shape='none', + style='filled', + margin='0', + orientation='0' if connector.ports_left else '180', + label='''< + + + + {colorbar} + +
{infostring_l} {infostring_r}
+ + + >'''.format(infostring_l=infostring_l, infostring_r=infostring_r, colorbar=colorbar)) + + else: # not a ferrule + identification = [connector.manufacturer, + f'MPN: {connector.manufacturer_part_number}' if connector.manufacturer_part_number else '', + f'IPN: {connector.internal_part_number}' if connector.internal_part_number else ''] + + 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'{pinnumber}') + if connector.ports_right: + pinouts[2].append(f'{pinnumber}') + label = [connector.name if connector.show_name else '', identification, 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: + 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 = '' + if cable.show_equiv: + # Only convert units we actually know about, i.e. currently + # mm2 and awg --- other units _are_ technically allowed, + # and passed through as-is. + if cable.gauge_unit =='mm\u00B2': + awg_fmt = f' ({awg_equiv(cable.gauge)} AWG)' + elif cable.gauge_unit.upper() == 'AWG': + awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' + + identification = [cable.manufacturer if not isinstance(cable.manufacturer, list) else '', + f'MPN: {cable.manufacturer_part_number}' if (cable.manufacturer_part_number and not isinstance(cable.manufacturer_part_number, list)) else '', + f'IPN: {cable.internal_part_number}' if (cable.internal_part_number and not isinstance(cable.internal_part_number, list)) else ''] + identification = list(filter(None, identification)) + + attributes = [f'{len(cable.colors)}x' if cable.show_wirecount else '', + f'{cable.gauge} {cable.gauge_unit}{awg_fmt}' if cable.gauge else '', + '+ S' if cable.shield else '', + f'{cable.length} m' if cable.length > 0 else ''] + attributes = list(filter(None, attributes)) + + html = '' # name+attributes table + + html = f'{html}' # spacer between attributes and wires + + html = f'{html}' # main table + if cable.notes: + html = f'{html}' # notes table + html = f'{html}' # spacer at the end + + html = f'{html}
' # main table + + html = f'{html}' # name+attributes table + if cable.show_name: + html = f'{html}' + if(len(identification) > 0): # print an identification row if values specified + html = f'{html}' # end identification row + html = f'{html}' # attribute row + for attrib in attributes: + html = f'{html}' + html = f'{html}' # attribute row + html = f'{html}
{cable.name}
' + for attrib in identification: + html = f'{html}' + html = f'{html}
{attrib}
{attrib}
 
' # conductor table + + for i, connection in enumerate(cable.colors, 1): + p = [] + p.append(f'') + p.append(wv_colors.translate_color(connection, self.color_mode)) + p.append(f'') + html = f'{html}' + for bla in p: + html = f'{html}' + html = f'{html}' + bgcolor = wv_colors.translate_color(connection, 'hex') + bgcolor = bgcolor if bgcolor != '' else '#ffffff' + html = f'{html}' + if(cable.category == 'bundle'): # for bundles individual wires can have part information + # create a list of wire parameters + wireidentification = [] + if isinstance(cable.manufacturer, list): + wireidentification.append(cable.manufacturer[i - 1]) + if isinstance(cable.manufacturer_part_number, list): + wireidentification.append(f'MPN: {cable.manufacturer_part_number[i - 1]}') + if isinstance(cable.internal_part_number, list): + wireidentification.append(f'IPN: {cable.internal_part_number[i - 1]}') + # print parameters into a table row under the wire + if(len(wireidentification) > 0): + html = f'{html}' + + if cable.shield: + p = ['', 'Shield', ''] + html = f'{html}' # spacer + html = f'{html}' + for bla in p: + html = html + f'' + html = f'{html}' + html = f'{html}' + + html = f'{html}' # spacer at the end + + html = f'{html}
{bla}
' + for attrib in wireidentification: + html = f'{html}' + html = f'{html}
{attrib}
 
{bla}
 
' # conductor table + + html = f'{html}
{cable.notes}
 
' # 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' + 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'', from_string) + if connection.to_port is not None: # connect to right + to_ferrule = self.connectors[connection.to_name].category == 'ferrule' + 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'', 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 + graph = self.create_graph() + for f in fmt: + graph.format = f + graph.render(filename=filename, directory=directory, view=view, cleanup=cleanup) + graph.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('') + + file.write('

Diagram

') + with open(f'{filename}.svg') as svg: + for svgdata in svg: + file.write(svgdata) + + file.write('

Bill of Materials

') + listy = flatten2d(bom_list) + file.write('') + file.write('') + for item in listy[0]: + file.write(f'') + file.write('') + for row in listy[1:]: + file.write('') + for i, item in enumerate(row): + align = 'align="right"' if listy[0][i] == 'Qty' else '' + file.write(f'') + file.write('') + file.write('
{item}
{item}
') + + file.write('') + + def bom(self): + bom = [] + bom_connectors = [] + bom_cables = [] + # connectors + types = Counter([(v.type, v.subtype, v.pincount, v.manufacturer, v.manufacturer_part_number, v.internal_part_number) 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, v.manufacturer, v.manufacturer_part_number, v.internal_part_number) == 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 ''} + if shared.manufacturer is not None: # set manufacturer only if it exists + item['manufacturer'] = shared.manufacturer + if shared.manufacturer_part_number is not None: # set part number only if it exists + item['manufacturer part number'] = shared.manufacturer_part_number + if shared.internal_part_number is not None: # set part number only if it exists + item['internal part number'] = shared.internal_part_number + 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, + v.manufacturer if not isinstance(v.manufacturer, list) else None, + v.manufacturer_part_number if not isinstance(v.manufacturer_part_number, list) else None, + v.internal_part_number if not isinstance(v.manufacturer_part_number, list) else None + ) 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, + v.manufacturer if not isinstance(v.manufacturer, list) else None, + v.manufacturer_part_number if not isinstance(v.manufacturer_part_number, list) else None, + v.internal_part_number if not isinstance(v.manufacturer_part_number, list) else None) == 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} + if shared.manufacturer is not None: # set manufacturer only if it exists + item['manufacturer'] = shared.manufacturer + if shared.manufacturer_part_number is not None: # set part number only if it exists + item['manufacturer part number'] = shared.manufacturer_part_number + if shared.internal_part_number is not None: # set part number only if it exists + item['internal part number'] = shared.internal_part_number + 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 index, color in enumerate(bundle.colors, 0): + wireinfo = {'gauge': shared.gauge, 'gauge_unit': shared.gauge_unit, 'length': shared.length, 'color': color, 'designator': bundle.name} + wireinfo['manufacturer'] = bundle.manufacturer[index] if isinstance(bundle.manufacturer, list) else None + wireinfo['manufacturer part number'] = bundle.manufacturer_part_number[index] if isinstance(bundle.manufacturer_part_number, list) else None + wireinfo['internal part number'] = bundle.internal_part_number[index] if isinstance(bundle.internal_part_number, list) else None + wirelist.append(wireinfo) + + # join similar wires from all the bundles to a single BOM item + types = Counter([(v['gauge'], v['gauge_unit'], v['color'], + v['manufacturer'], v['manufacturer part number'], v['internal part number']) for v in wirelist]) + for maintype in types: + items = [v for v in wirelist if ( + v['gauge'], v['gauge_unit'], v['color'], + v['manufacturer'], v['manufacturer part number'], v['internal part number']) == 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} + if shared['manufacturer'] is not None: # set manufacturer only if it exists + item['manufacturer'] = shared['manufacturer'] + if shared['manufacturer part number'] is not None: # set part number only if it exists + item['manufacturer part number'] = shared['manufacturer part number'] + if shared['internal part number'] is not None: # set part number only if it exists + item['internal part number'] = shared['internal part number'] + 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'] + # check if any optional fields are set and add to keys if they are + for fieldname in ["manufacturer", "manufacturer part number", "internal part number"]: + if any(fieldname in x for x in bom): + keys.append(fieldname) + 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 diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index fc04fce..078e4e2 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +# -*- coding: utf-8 -*- import os import sys diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index be731bf..27d9d55 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -1,540 +1,18 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- import argparse -from collections import Counter -from dataclasses import dataclass, field -from graphviz import Graph import os import sys -from typing import Any, List + import yaml -if __name__== '__main__': +if __name__ == '__main__': sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from wireviz import wv_colors -from wireviz.wv_helper import nested, int2tuple, awg_equiv, flatten2d, tuplelist2tsv -class Harness: +from wireviz.Harness import 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 '') - # id = identification - id = [n.manufacturer, - 'MPN: {}'.format(n.manufacturer_part_number) if n.manufacturer_part_number else '', - 'IPN: {}'.format(n.internal_part_number) if n.internal_part_number else ''] - id = list(filter(None, id)) - if(len(id) > 0): - infostring = infostring + '
' - for attrib in id: - infostring = infostring + '{attrib}, '.format(attrib=attrib) - infostring = infostring[:-2] # remove trainling comma and space - - 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='''< - - - - {colorbar} - -
{infostring_l} {infostring_r}
- - - >'''.format(infostring_l=infostring_l, - infostring_r=infostring_r, - colorbar=''.format(wv_colors.translate_color(n.color, 'HEX')) if n.color else '')) - - else: # not a ferrule - # id = identification - id = [n.manufacturer, - 'MPN: {}'.format(n.manufacturer_part_number) if n.manufacturer_part_number else '', - 'IPN: {}'.format(n.internal_part_number) if n.internal_part_number else ''] - # 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('{portno}'.format(portno=pinnumber)) - if n.ports_right: - p[2].append('{portno}'.format(portno=pinnumber)) - # l = label - l = [n.name if n.show_name else '', id, 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(): - # id = identification - id = [c.manufacturer if not isinstance(c.manufacturer, list) else '', - 'MPN: {}'.format(c.manufacturer_part_number) if (c.manufacturer_part_number and not isinstance(c.manufacturer_part_number, list)) else '', - 'IPN: {}'.format(c.internal_part_number) if (c.internal_part_number and not isinstance(c.internal_part_number, list)) else ''] - id = list(filter(None, id)) - # 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 = '' # name+attributes table - - html = html + '' # spacer between attributes and wires - - html = html + '' # main table - if c.notes: - html = html + ''.format(c.notes) # notes table - html = html + '' # spacer at the end - - html = html + '
' # main table - - html = html + '' # name+attributes table - if c.show_name: - html = html + ''.format(colspan=len(a), name=c.name) - if(len(id) > 0): # print an identification row if values specified - html = html + '' # end identification row - html = html + '' # attribute row - for attrib in a: - html = html + ''.format(attrib=attrib) - html = html + '' # attribute row - html = html + '
{name}
'.format(colspan=len(a)) - for attrib in id: - html = html + ''.format(attrib=attrib) - html = html + '
{attrib}
{attrib}
 
' # conductor table - - for i, x in enumerate(c.colors,1): - p = [] - p.append(''.format(i)) - p.append(wv_colors.translate_color(x, self.color_mode)) - p.append(''.format(i)) - html = html + '' - for bla in p: - html = html + ''.format(bla) - html = html + '' - bgcolor = wv_colors.translate_color(x, 'hex') - html = html + ''.format(colspan=len(p), bgcolor=bgcolor if bgcolor != '' else '#ffffff', port='w{}'.format(i)) - if(c.category == 'bundle'): # for bundles individual wires can have part information - # create a list of wire parameters - wireid = [] - if isinstance(c.manufacturer, list): - wireid.append(c.manufacturer[i - 1]) - if isinstance(c.manufacturer_part_number, list): - wireid.append('MPN: {}'.format(c.manufacturer_part_number[i - 1])) - if isinstance(c.internal_part_number, list): - wireid.append('IPN: {}'.format(c.internal_part_number[i - 1])) - # print parameters into a table row under the wire - if(len(wireid) > 0): - html = html + '' - - if c.shield: - p = ['', 'Shield', ''] - html = html + '' # spacer - html = html + '' - for bla in p: - html = html + ''.format(bla) - html = html + '' - html = html + ''.format(colspan=len(p), bgcolor=wv_colors.translate_color(x, 'hex'), port='ws') - - html = html + '' # spacer at the end - - html = html + '
{}
'.format(colspan=len(a)) - for attrib in wireid: - html = html + ''.format(attrib=attrib) - html = html + '
{attrib}
 
{}
 
' # conductor table - - html = html + '
{}
 
' # 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(''.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(''.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('') - - file.write('

Diagram

') - with open('{}.svg'.format(filename),'r') as svg: - for l in svg: - file.write(l) - - file.write('

Bill of Materials

') - listy = flatten2d(bom_list) - file.write('') - file.write('') - for item in listy[0]: - file.write(''.format(item)) - file.write('') - for row in listy[1:]: - file.write('') - for i, item in enumerate(row): - file.write(''.format(content=item, align='align="right"' if listy[0][i] == 'Qty' else '')) - file.write('') - file.write('
{}
{content}
') - - file.write('') - - def bom(self): - bom = [] - bom_connectors = [] - bom_cables = [] - # connectors - types = Counter([(v.type, v.subtype, v.pincount, v.manufacturer, v.manufacturer_part_number, v.internal_part_number) 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, v.manufacturer, v.manufacturer_part_number, v.internal_part_number) == 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 ''} - if shared.manufacturer is not None: # set manufacturer only if it exists - item['manufacturer'] = shared.manufacturer - if shared.manufacturer_part_number is not None: # set part number only if it exists - item['manufacturer part number'] = shared.manufacturer_part_number - if shared.internal_part_number is not None: # set part number only if it exists - item['internal part number'] = shared.internal_part_number - 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, - v.manufacturer if not isinstance(v.manufacturer, list) else None, - v.manufacturer_part_number if not isinstance(v.manufacturer_part_number, list) else None, - v.internal_part_number if not isinstance(v.manufacturer_part_number, list) else None - ) 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, - v.manufacturer if not isinstance(v.manufacturer, list) else None, - v.manufacturer_part_number if not isinstance(v.manufacturer_part_number, list) else None, - v.internal_part_number if not isinstance(v.manufacturer_part_number, list) else None - ) == 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} - if shared.manufacturer is not None: # set manufacturer only if it exists - item['manufacturer'] = shared.manufacturer - if shared.manufacturer_part_number is not None: # set part number only if it exists - item['manufacturer part number'] = shared.manufacturer_part_number - if shared.internal_part_number is not None: # set part number only if it exists - item['internal part number'] = shared.internal_part_number - 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 index, color in enumerate(bundle.colors, 0): - wireinfo = {'gauge': shared.gauge, 'gauge_unit': shared.gauge_unit, 'length': shared.length, 'color': color, 'designator': bundle.name} - wireinfo['manufacturer'] = bundle.manufacturer[index] if isinstance(bundle.manufacturer, list) else None - wireinfo['manufacturer part number'] = bundle.manufacturer_part_number[index] if isinstance(bundle.manufacturer_part_number, list) else None - wireinfo['internal part number'] = bundle.internal_part_number[index] if isinstance(bundle.internal_part_number, list) else None - wirelist.append(wireinfo) - # join similar wires from all the bundles to a single BOM item - types = Counter([(v['gauge'], v['gauge_unit'], v['color'], v['manufacturer'], v['manufacturer part number'], v['internal part number']) for v in wirelist]) - for type in types: - items = [v for v in wirelist if (v['gauge'], v['gauge_unit'], v['color'], v['manufacturer'], v['manufacturer part number'], v['internal part number']) == 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} - if shared['manufacturer'] is not None: # set manufacturer only if it exists - item['manufacturer'] = shared['manufacturer'] - if shared['manufacturer part number'] is not None: # set part number only if it exists - item['manufacturer part number'] = shared['manufacturer part number'] - if shared['internal part number'] is not None: # set part number only if it exists - item['internal part number'] = shared['internal part number'] - 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'] - # check if any optional fields are set and add to keys if they are - for fieldname in ["manufacturer", "manufacturer part number", "internal part number"]: - if any(fieldname in x for x in bom): - keys.append(fieldname) - 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 - manufacturer: str = None - manufacturer_part_number: str = None - internal_part_number: str = None - 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 - manufacturer: str = None - manufacturer_part_number: str = None - internal_part_number: str = None - 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) - - # if lists of part numbers are provided check this is a bundle and that it matches the wirecount. - for idfield in [self.manufacturer, self.manufacturer_part_number, self.internal_part_number]: - if isinstance(idfield, list): - if self.category == "bundle": - # check the length - if len(idfield) != self.wirecount: - raise Exception('lists of part data must match wirecount') - else: - raise Exception('lists of part data are only supported for bundles') - - # 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): @@ -547,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 output = [] if not isinstance(yaml_data, list): - yaml_data = [yaml_data,] + yaml_data = [yaml_data] for e in yaml_data: e = str(e) - if '-' in e: # list of pins + if '-' in e: # list of pins a, b = tuple(map(int, e.split('-'))) if a < b: - for x in range(a,b+1): + for x in range(a, b + 1): output.append(x) elif a > b: - for x in range(a,b-1,-1): + for x in range(a, b - 1, -1): output.append(x) elif a == b: output.append(a) else: try: x = int(e) - except: + except Exception: x = e output.append(x) return output @@ -574,25 +52,25 @@ def parse(yaml_input, file_out=None, generate_bom=False): return False return True - h = Harness() + harness = Harness() # add items - sections = ['connectors','cables','ferrules','connections'] - types = [dict, dict, dict, list] + sections = ['connectors', 'cables', 'ferrules', 'connections'] + types = [dict, dict, dict, list] for sec, ty in zip(sections, types): if sec in yaml_data and type(yaml_data[sec]) == ty: if len(yaml_data[sec]) > 0: if ty == dict: - for k, o in yaml_data[sec].items(): + for key, o in yaml_data[sec].items(): if sec == 'connectors': - h.add_connector(name=k, **o) + harness.add_connector(name=key, **o) elif sec == 'cables': - h.add_cable(name=k, **o) + harness.add_cable(name=key, **o) elif sec == 'ferrules': pass else: - pass # section exists but is empty - else: # section does not exist, create empty section + pass # section exists but is empty + else: # section does not exist, create empty section if ty == dict: yaml_data[sec] = {} elif ty == list: @@ -600,64 +78,63 @@ def parse(yaml_input, file_out=None, generate_bom=False): # add connections ferrule_counter = 0 - for con in yaml_data['connections']: - if len(con) == 3: # format: connector -- cable -- connector + for connections in yaml_data['connections']: + if len(connections) == 3: # format: connector -- cable -- connector - for c in con: - if len(list(c.keys())) != 1: # check that each entry in con has only one key, which is the designator + for connection in connections: + 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') - from_name = list(con[0].keys())[0] - via_name = list(con[1].keys())[0] - to_name = list(con[2].keys())[0] + from_name = list(connections[0].keys())[0] + via_name = list(connections[1].keys())[0] + to_name = list(connections[2].keys())[0] - if not check_designators([from_name,via_name,to_name],('connectors','cables','connectors')): - print([from_name,via_name,to_name]) + if not check_designators([from_name, via_name, to_name], ('connectors', 'cables', 'connectors')): + print([from_name, via_name, to_name]) raise Exception('Bad connection definition (3)') - from_pins = expand(con[0][from_name]) - via_pins = expand(con[1][via_name]) - to_pins = expand(con[2][to_name]) + from_pins = expand(connections[0][from_name]) + via_pins = expand(connections[1][via_name]) + to_pins = expand(connections[2][to_name]) if len(from_pins) != len(via_pins) or len(via_pins) != len(to_pins): raise Exception('List length mismatch') 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: - if type(c) is dict: - if len(list(c.keys())) != 1: # check that each entry in con has only one key, which is the designator + for connection in connections: + if type(connection) is dict: + 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') # hack to make the format for ferrules compatible with the formats for connectors and cables - if type(con[0]) == str: - name = con[0] - con[0] = {} - con[0][name] = name - if type(con[1]) == str: - name = con[1] - con[1] = {} - con[1][name] = name + if type(connections[0]) == str: + name = connections[0] + connections[0] = {} + connections[0][name] = name + if type(connections[1]) == str: + name = connections[1] + connections[1] = {} + connections[1][name] = name - from_name = list(con[0].keys())[0] - to_name = list(con[1].keys())[0] + from_name = list(connections[0].keys())[0] + to_name = list(connections[1].keys())[0] - con_cbl = check_designators([from_name, to_name],('connectors','cables')) - cbl_con = check_designators([from_name, to_name],('cables','connectors')) - con_con = check_designators([from_name, to_name],('connectors','connectors')) + con_cbl = check_designators([from_name, to_name], ('connectors', 'cables')) + cbl_con = check_designators([from_name, to_name], ('cables', 'connectors')) + con_con = check_designators([from_name, to_name], ('connectors', 'connectors')) - - fer_cbl = check_designators([from_name, to_name],('ferrules','cables')) - cbl_fer = check_designators([from_name, to_name],('cables','ferrules')) + fer_cbl = check_designators([from_name, to_name], ('ferrules', 'cables')) + 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: raise Exception('Wrong designators') - from_pins = expand(con[0][from_name]) - to_pins = expand(con[1][to_name]) + from_pins = expand(connections[0][from_name]) + to_pins = expand(connections[1][to_name]) if con_cbl or cbl_con or con_con: if len(from_pins) != len(to_pins): @@ -666,19 +143,19 @@ def parse(yaml_input, file_out=None, generate_bom=False): if con_cbl or cbl_con: for (from_pin, to_pin) in zip(from_pins, to_pins): if con_cbl: - h.connect(from_name, from_pin, to_name, to_pin, None, None) - else: # cbl_con - h.connect(None, None, from_name, from_pin, to_name, to_pin) + harness.connect(from_name, from_pin, to_name, to_pin, None, None) + else: # cbl_con + harness.connect(None, None, from_name, from_pin, to_name, to_pin) elif con_con: - cocon_coname = list(con[0].keys())[0] - from_pins = expand(con[0][from_name]) - to_pins = expand(con[1][to_name]) + cocon_coname = list(connections[0].keys())[0] + from_pins = expand(connections[0][from_name]) + to_pins = expand(connections[1][to_name]) 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: - from_pins = expand(con[0][from_name]) - to_pins = expand(con[1][to_name]) + from_pins = expand(connections[0][from_name]) + to_pins = expand(connections[1][to_name]) if fer_cbl: ferrule_name = from_name @@ -692,19 +169,19 @@ def parse(yaml_input, file_out=None, generate_bom=False): ferrule_params = yaml_data['ferrules'][ferrule_name] for cable_pin in cable_pins: ferrule_counter = ferrule_counter + 1 - ferrule_id = '_F{}'.format(ferrule_counter) - h.add_connector(ferrule_id, category='ferrule', **ferrule_params) + ferrule_id = f'_F{ferrule_counter}' + harness.add_connector(ferrule_id, category='ferrule', **ferrule_params) 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: - h.connect(None, None, cable_name, cable_pin, ferrule_id, 1) - + harness.connect(None, None, cable_name, cable_pin, ferrule_id, 1) else: 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): with open(yaml_file, 'r') as file: @@ -720,27 +197,21 @@ def parse_file(yaml_file, file_out=None, generate_bom=False): def parse_cmdline(): 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('-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('--prepend-file', action='store', type=str, metavar='YAML_FILE') + return parser.parse_args() - args = parser.parse_args() - - return args def main(): args = parse_cmdline() 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) with open(args.input_file) as fh: @@ -748,7 +219,7 @@ def main(): if 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) with open(args.prepend_file) as fh: prepend = fh.read() @@ -757,12 +228,13 @@ def main(): if not args.output_file: file_out = args.input_file 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: file_out = args.output_file file_out = os.path.abspath(file_out) parse(yaml_input, file_out=file_out, generate_bom=args.generate_bom) + if __name__ == '__main__': main() diff --git a/src/wireviz/wv_colors.py b/src/wireviz/wv_colors.py index 32c90c9..055b467 100644 --- a/src/wireviz/wv_colors.py +++ b/src/wireviz/wv_colors.py @@ -1,74 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + COLOR_CODES = { - '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'], - 'BW': ['BK','WH'] - } + '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'], + 'BW': ['BK', 'WH'], +} color_hex = { - 'BK': '#000000', - 'WH': '#ffffff', - 'GY': '#999999', - 'PK': '#ff66cc', - 'RD': '#ff0000', - 'OG': '#ff8000', - 'YE': '#ffff00', - 'GN': '#00ff00', - 'TQ': '#00ffff', - 'BU': '#0066ff', - 'VT': '#8000ff', - 'BN': '#666600', - } + 'BK': '#000000', + 'WH': '#ffffff', + 'GY': '#999999', + 'PK': '#ff66cc', + 'RD': '#ff0000', + 'OG': '#ff8000', + 'YE': '#ffff00', + 'GN': '#00ff00', + 'TQ': '#00ffff', + 'BU': '#0066ff', + 'VT': '#8000ff', + 'BN': '#666600', +} color_full = { - 'BK': 'black', - 'WH': 'white', - 'GY': 'grey', - 'PK': 'pink', - 'RD': 'red', - 'OG': 'orange', - 'YE': 'yellow', - 'GN': 'green', - 'TQ': 'turquoise', - 'BU': 'blue', - 'VT': 'violet', - 'BN': 'brown', + 'BK': 'black', + 'WH': 'white', + 'GY': 'grey', + 'PK': 'pink', + 'RD': 'red', + 'OG': 'orange', + 'YE': 'yellow', + 'GN': 'green', + 'TQ': 'turquoise', + 'BU': 'blue', + 'VT': 'violet', + 'BN': 'brown', } color_ger = { - 'BK': 'sw', - 'WH': 'ws', - 'GY': 'gr', - 'PK': 'rs', - 'RD': 'rt', - 'OG': 'or', - 'YE': 'ge', - 'GN': 'gn', - 'TQ': 'tk', - 'BU': 'bl', - 'VT': 'vi', - 'BN': 'br', + 'BK': 'sw', + 'WH': 'ws', + 'GY': 'gr', + 'PK': 'rs', + 'RD': 'rt', + 'OG': 'or', + 'YE': 'ge', + 'GN': 'gn', + 'TQ': 'tk', + 'BU': 'bl', + 'VT': 'vi', + 'BN': 'br', } -def translate_color(input, color_mode): - if input == '': + +def translate_color(inp, color_mode): + if inp == '': output = '' else: if color_mode == 'full': - output = color_full[input].lower() + output = color_full[inp].lower() elif color_mode == 'FULL': - output = color_full[input].upper() + output = color_full[inp].upper() elif color_mode == 'hex': - output = color_hex[input].lower() + output = color_hex[inp].lower() elif color_mode == 'HEX': - output = color_hex[input].upper() + output = color_hex[inp].upper() elif color_mode == 'ger': - output = color_ger[input].lower() + output = color_ger[inp].lower() elif color_mode == 'GER': - output = color_ger[input].upper() + output = color_ger[inp].upper() elif color_mode == 'short': - output = input.lower() + output = inp.lower() elif color_mode == 'SHORT': - output = input.upper() + output = inp.upper() else: raise Exception('Unknown color mode') return output diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 1c803fd..222fd74 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -1,33 +1,38 @@ -from typing import Any, List +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import List + +awg_equiv_table = { + '0.09': '28', + '0.14': '26', + '0.25': '24', + '0.34': '22', + '0.5': '21', + '0.75': '20', + '1': '18', + '1.5': '16', + '2.5': '14', + '4': '12', + '6': '10', + '10': '8', + '16': '6', + '25': '4', + '35': '2', + '50': '1', +} + +mm2_equiv_table = {v:k for k,v in awg_equiv_table.items()} def awg_equiv(mm2): - awg_equiv_table = { - '0.09': 28, - '0.14': 26, - '0.25': 24, - '0.34': 22, - '0.5': 21, - '0.75': 20, - '1': 18, - '1.5': 16, - '2.5': 14, - '4': 12, - '6': 10, - '10': 8, - '16': 6, - '25': 4, - '35': 2, - '50': 1, - } - k = str(mm2) - if k in awg_equiv_table: - return awg_equiv_table[k] - else: - return 'unknown' + return awg_equiv_table.get(str(mm2), 'Unknown') -def nested(input): +def mm2_equiv(awg): + return mm2_equiv_table.get(str(awg), 'Unknown') + +def nested(inp): l = [] - for x in input: + for x in inp: if isinstance(x, list): if len(x) > 0: n = nested(x) @@ -37,25 +42,26 @@ def nested(input): if x is not None: if x != '': l.append(str(x)) - s = '|'.join(l) - return s + return '|'.join(l) -def int2tuple(input): - if isinstance(input, tuple): - output = input + +def int2tuple(inp): + if isinstance(inp, tuple): + output = inp else: - output = (input,) + output = (inp,) 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 = '' if header is not None: - input.insert(0, header) - input = flatten2d(input) - for row in input: + inp.insert(0, header) + inp = flatten2d(inp) + for row in inp: output = output + '\t'.join(str(item) for item in row) + '\n' return output