diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8ddf91b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Change Log + +## [0.2](https://github.com/formatc1702/WireViz/tree/v0.2) (2020-XX-XX) + +### Backward incompatible changes + +- Change names of connector attributes ([#77](https://github.com/formatc1702/WireViz/issues/77)) +- Remove ferrules as a separate connector type ([#78](https://github.com/formatc1702/WireViz/issues/78)) +- Change the way loops are defined ([#79](https://github.com/formatc1702/WireViz/issues/79)) + +### New features +- Add bidirectional AWG/mm2 conversion ([#41](https://github.com/formatc1702/WireViz/pull/41)) +- Add support for part numbers ([#11](https://github.com/formatc1702/WireViz/pull/11)) +- Add support for multicolored wires ([#17](https://github.com/formatc1702/WireViz/pull/17)) +- Add ability to export data directly to other programs ([#55](https://github.com/formatc1702/WireViz/pull/55)) +- Add support for line breaks in various fields ([#63](https://github.com/formatc1702/WireViz/issues/63)) +- Allow using connector pin names to define connections ([#72](https://github.com/formatc1702/WireViz/issues/72)) +- Make defining connection sets easier and more flexible ([#67](https://github.com/formatc1702/WireViz/issues/67)) + +### Misc. fixes + +- Improve BOM generation +- Add various input sanity checks + +### + +## [0.1](https://github.com/formatc1702/WireViz/tree/v0.1) (2020-06-29) + +- Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7a94ac7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Contribution Guidelines + +When contributing to this repository, please first discuss the change you +wish to make via issue, email, or any other method with the owners of this +repository before making a change. + +## Pull Requests + +1. Fork this repository to your repository +1. Clone the repository to your local machine +1. Checkout the `dev` branch +1. Make any changes to the code on the `dev` branch +1. Push your changes to your fork +1. Create new pull request + +## Documentation Strings + +Documentation strings are to follow the Google Style ([examples](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)). diff --git a/README.md b/README.md index b96dd42..22c9a49 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,13 @@ If you would like to rebuild all of the included demos, examples and tutorials, ```cd src/wireviz ./build_examples.py + ``` + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) + ## 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. @@ -123,4 +129,4 @@ Ubuntu 18.04 LTS users in particular may need to separately install Python 3.7 o ## License -GNU GPLv3 +[GPL-3.0](LICENSE) diff --git a/examples/demo02.yml b/examples/demo02.yml index 00ee554..600b135 100644 --- a/examples/demo02.yml +++ b/examples/demo02.yml @@ -9,11 +9,6 @@ templates: # defining templates to be used later on gauge: 0.14 mm2 colors: [BK, RD, YE, GN] -ferrules: - ferrule_crimp: - type: Crimp ferrule - subtype: 0.25 mm² - connectors: X1: <<: *molex_f # copying items from the template @@ -27,6 +22,11 @@ connectors: X4: <<: *molex_f pinout: [GND, +12V, MISO, MOSI, SCK] + ferrule_crimp: + category: ferrule + autogenerate: true + type: Crimp ferrule + subtype: 0.25 mm² cables: W1: diff --git a/examples/ex01.bom.tsv b/examples/ex01.bom.tsv index 2beb9f6..8af2691 100644 --- a/examples/ex01.bom.tsv +++ b/examples/ex01.bom.tsv @@ -1,3 +1,3 @@ Item Qty Unit Designators Connector, Molex KK 254, female, 4 pins 2 X1, X2 -Cable, 4 x 0.25 mm² shielded 0.2 m W1 +Cable, Serial, 4 x 0.25 mm² shielded 0.2 m W1 diff --git a/examples/ex01.gv b/examples/ex01.gv index 8e156b0..d1ccbd3 100644 --- a/examples/ex01.gv +++ b/examples/ex01.gv @@ -1,12 +1,12 @@ graph { // Graph generated by WireViz // https://github.com/formatc1702/WireViz - graph [bgcolor="#fffbf8" fontname=arial nodesep=0.33 rankdir=LR ranksep=2] + 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] X1 [label="X1|{Molex KK 254|female|4-pin}|{{GND|VCC|RX|TX}|{1|2|3|4}}"] X2 [label="X2|{Molex KK 254|female|4-pin}|{{1|2|3|4}|{GND|VCC|RX|TX}}"] - edge [color="#000000:#a52a2a:#000000"] + edge [color="#000000:#666600:#000000"] X1:p1r:e -- W1:w1:w W1:w1:e -- X2:p1l:w edge [color="#000000:#ff0000:#000000"] @@ -18,7 +18,7 @@ graph { edge [color="#000000:#ffff00:#000000"] X1:p4r:e -- W1:w4:w W1:w4:e -- X2:p3l:w - edge [color="#aaaaaa:#84878c"] + edge [color="#000000"] X1:p1r:e -- W1:ws:w - W1 [label=<
W1
4x0.25 mm² (24 AWG)+ S0.2 m
X1:1BNX2:1
X1:2RDX2:2
X1:3OGX2:4
X1:4YEX2:3
X1:1Shield
> fillcolor=white margin=0 shape=box style=""] + W1 [label=<
W1
Serial4x0.25 mm² (24 AWG)+ S0.2 m
 
X1:1BNX2:1
X1:2RDX2:2
X1:3OGX2:4
X1:4YEX2:3
 
X1:1Shield
 
> fillcolor=white margin=0 shape=box style=""] } diff --git a/examples/ex01.html b/examples/ex01.html index d69a2eb..6e7d4d8 100644 --- a/examples/ex01.html +++ b/examples/ex01.html @@ -1,169 +1,177 @@

Diagram

- - - - -%3 - + + + + -X1 - -X1 - -Molex KK 254 - -female - -4-pin - -GND - -VCC - -RX - -TX - -1 - -2 - -3 - -4 + +X1 + +X1 + +Molex KK 254 + +female + +4-pin + +GND + +VCC + +RX + +TX + +1 + +2 + +3 + +4 -W1 - - -W1 - -4x - -0.25 mm² (24 AWG) - -+ S - -0.2 m -X1:1 -BN -X2:1 - - - -X1:2 -RD -X2:2 - - - -X1:3 -OG -X2:4 - - - -X1:4 -YE -X2:3 - - - -X1:1 -Shield - - - - - - - - + +W1 + + +W1 + +Serial + +4x + +0.25 mm² (24 AWG) + ++ S + +0.2 m +  +X1:1 +BN +X2:1 + + + +X1:2 +RD +X2:2 + + + +X1:3 +OG +X2:4 + + + +X1:4 +YE +X2:3 + + + +  +X1:1 +Shield + +  -X1:p1r:e--W1:w1:w - - - + +X1:e--W1:w + + + -X1:p2r:e--W1:w2:w - - - + +X1:e--W1:w + + + -X1:p3r:e--W1:w3:w - - - + +X1:e--W1:w + + + -X1:p4r:e--W1:w4:w - - - + +X1:e--W1:w + + + -X1:p1r:e--W1:ws:w - - + +X1:e--W1:w + -X2 - -X2 - -Molex KK 254 - -female - -4-pin - -1 - -2 - -3 - -4 - -GND - -VCC - -RX - -TX + +X2 + +X2 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + +GND + +VCC + +RX + +TX -W1:w1:e--X2:p1l:w - - - + +W1:e--X2:w + + + -W1:w2:e--X2:p2l:w - - - + +W1:e--X2:w + + + -W1:w3:e--X2:p4l:w - - - + +W1:e--X2:w + + + -W1:w4:e--X2:p3l:w - - - + +W1:e--X2:w + + + -

Bill of Materials

ItemQtyUnitDesignators
Connector, Molex KK 254, female, 4 pins2X1, X2
Cable, 4 x 0.25 mm² shielded0.2mW1
\ No newline at end of file +

Bill of Materials

ItemQtyUnitDesignators
Connector, Molex KK 254, female, 4 pins2X1, X2
Cable, Serial, 4 x 0.25 mm² shielded0.2mW1
\ No newline at end of file diff --git a/examples/ex01.png b/examples/ex01.png index 7854b2d..f9fa61f 100644 Binary files a/examples/ex01.png and b/examples/ex01.png differ diff --git a/examples/ex01.svg b/examples/ex01.svg index d6247ed..8d5dbbb 100644 --- a/examples/ex01.svg +++ b/examples/ex01.svg @@ -1,168 +1,176 @@ - - - - -%3 - + + + + -X1 - -X1 - -Molex KK 254 - -female - -4-pin - -GND - -VCC - -RX - -TX - -1 - -2 - -3 - -4 + +X1 + +X1 + +Molex KK 254 + +female + +4-pin + +GND + +VCC + +RX + +TX + +1 + +2 + +3 + +4 -W1 - - -W1 - -4x - -0.25 mm² (24 AWG) - -+ S - -0.2 m -X1:1 -BN -X2:1 - - - -X1:2 -RD -X2:2 - - - -X1:3 -OG -X2:4 - - - -X1:4 -YE -X2:3 - - - -X1:1 -Shield - - - - - - - - + +W1 + + +W1 + +Serial + +4x + +0.25 mm² (24 AWG) + ++ S + +0.2 m +  +X1:1 +BN +X2:1 + + + +X1:2 +RD +X2:2 + + + +X1:3 +OG +X2:4 + + + +X1:4 +YE +X2:3 + + + +  +X1:1 +Shield + +  -X1:p1r:e--W1:w1:w - - - + +X1:e--W1:w + + + -X1:p2r:e--W1:w2:w - - - + +X1:e--W1:w + + + -X1:p3r:e--W1:w3:w - - - + +X1:e--W1:w + + + -X1:p4r:e--W1:w4:w - - - + +X1:e--W1:w + + + -X1:p1r:e--W1:ws:w - - + +X1:e--W1:w + -X2 - -X2 - -Molex KK 254 - -female - -4-pin - -1 - -2 - -3 - -4 - -GND - -VCC - -RX - -TX + +X2 + +X2 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + +GND + +VCC + +RX + +TX -W1:w1:e--X2:p1l:w - - - + +W1:e--X2:w + + + -W1:w2:e--X2:p2l:w - - - + +W1:e--X2:w + + + -W1:w3:e--X2:p4l:w - - - + +W1:e--X2:w + + + -W1:w4:e--X2:p3l:w - - - + +W1:e--X2:w + + + diff --git a/examples/ex01.yml b/examples/ex01.yml index 83cce3c..9e4340f 100644 --- a/examples/ex01.yml +++ b/examples/ex01.yml @@ -16,6 +16,7 @@ cables: show_equiv: true # auto-calculate AWG equivalent from metric gauge length: 0.2 # length in m shield: true + type: Serial connections: - diff --git a/examples/ex04.yml b/examples/ex04.yml index b9eb707..3fe5bed 100644 --- a/examples/ex04.yml +++ b/examples/ex04.yml @@ -1,13 +1,3 @@ -# connectors: -# X1: -# type: D-Sub -# subtype: female -# pincount: 4 -# X2: -# type: Molex KK 254 -# subtype: female -# pincount: 3 - cables: W1: gauge: 0.25 mm2 @@ -17,8 +7,10 @@ cables: wirecount: 6 category: bundle -ferrules: +connectors: ferrule_crimp: + category: ferrule + autogenerate: true type: Crimp ferrule show_name: false show_pincount: false diff --git a/examples/ex05.gv b/examples/ex05.gv index 014df61..5507799 100644 --- a/examples/ex05.gv +++ b/examples/ex05.gv @@ -1,7 +1,7 @@ graph { // Graph generated by WireViz // https://github.com/formatc1702/WireViz - graph [bgcolor="#fffbf8" fontname=arial nodesep=0.33 rankdir=LR ranksep=2] + 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] X1 [label="X1|{Molex KK 254|female|4-pin}|{{GND|VCC|SCL|SDA}|{1|2|3|4}}"] @@ -19,7 +19,7 @@ graph { edge [color="#000000:#8000ff:#000000"] X1:p4r:e -- W1:w4:w W1:w4:e -- X2:p4l:w - W1 [label=<
W1
4x0.25 mm²0.2 m
X1:1PKX2:1
X1:2TQX2:2
X1:3YEX2:3
X1:4VTX2:4
> fillcolor=white margin=0 shape=box style="filled,dashed"] + W1 [label=<
W1
I2C4x0.25 mm²0.2 m
 
X1:1PKX2:1
X1:2TQX2:2
X1:3YEX2:3
X1:4VTX2:4
 
> fillcolor=white margin=0 shape=box style="filled,dashed"] edge [color="#000000:#ff66cc:#000000"] X2:p1r:e -- W2:w1:w W2:w1:e -- X3:p1l:w @@ -32,5 +32,5 @@ graph { edge [color="#000000:#8000ff:#000000"] X2:p4r:e -- W2:w4:w W2:w4:e -- X3:p4l:w - W2 [label=<
W2
4x0.25 mm²0.2 m
X2:1PKX3:1
X2:2TQX3:2
X2:3YEX3:3
X2:4VTX3:4
> fillcolor=white margin=0 shape=box style="filled,dashed"] + W2 [label=<
W2
I2C4x0.25 mm²0.2 m
 
X2:1PKX3:1
X2:2TQX3:2
X2:3YEX3:3
X2:4VTX3:4
 
> fillcolor=white margin=0 shape=box style="filled,dashed"] } diff --git a/examples/ex05.html b/examples/ex05.html index bf49d39..fc60fbc 100644 --- a/examples/ex05.html +++ b/examples/ex05.html @@ -1,271 +1,299 @@

Diagram

- - - - -%3 - + + + + -X1 - -X1 - -Molex KK 254 - -female - -4-pin - -GND - -VCC - -SCL - -SDA - -1 - -2 - -3 - -4 + +X1 + +X1 + +Molex KK 254 + +female + +4-pin + +GND + +VCC + +SCL + +SDA + +1 + +2 + +3 + +4 -W1 - - -W1 - -4x - -0.25 mm² - -0.2 m -X1:1 -PK -X2:1 - - - -X1:2 -TQ -X2:2 - - - -X1:3 -YE -X2:3 - - - -X1:4 -VT -X2:4 - - - + +W1 + + +W1 + +I2C + +4x + +0.25 mm² + +0.2 m +  +X1:1 +PK +X2:1 + + + +X1:2 +TQ +X2:2 + + + +X1:3 +YE +X2:3 + + + +X1:4 +VT +X2:4 + + + +  -X1:p1r:e--W1:w1:w - - - + +X1:e--W1:w + + + -X1:p2r:e--W1:w2:w - - - + +X1:e--W1:w + + + -X1:p3r:e--W1:w3:w - - - + +X1:e--W1:w + + + -X1:p4r:e--W1:w4:w - - - + +X1:e--W1:w + + + -X2 - -X2 - -Molex KK 254 - -female - -4-pin - -1 - -2 - -3 - -4 - -GND - -VCC - -SCL - -SDA - -1 - -2 - -3 - -4 + +X2 + +X2 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + +GND + +VCC + +SCL + +SDA + +1 + +2 + +3 + +4 -W2 - - -W2 - -4x - -0.25 mm² - -0.2 m -X2:1 -PK -X3:1 - - - -X2:2 -TQ -X3:2 - - - -X2:3 -YE -X3:3 - - - -X2:4 -VT -X3:4 - - - + +W2 + + +W2 + +I2C + +4x + +0.25 mm² + +0.2 m +  +X2:1 +PK +X3:1 + + + +X2:2 +TQ +X3:2 + + + +X2:3 +YE +X3:3 + + + +X2:4 +VT +X3:4 + + + +  -X2:p1r:e--W2:w1:w - - - + +X2:e--W2:w + + + -X2:p2r:e--W2:w2:w - - - + +X2:e--W2:w + + + -X2:p3r:e--W2:w3:w - - - + +X2:e--W2:w + + + -X2:p4r:e--W2:w4:w - - - + +X2:e--W2:w + + + -X3 - -X3 - -Molex KK 254 - -female - -4-pin - -1 - -2 - -3 - -4 - -GND - -VCC - -SCL - -SDA + +X3 + +X3 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + +GND + +VCC + +SCL + +SDA -W1:w1:e--X2:p1l:w - - - + +W1:e--X2:w + + + -W1:w2:e--X2:p2l:w - - - + +W1:e--X2:w + + + -W1:w3:e--X2:p3l:w - - - + +W1:e--X2:w + + + -W1:w4:e--X2:p4l:w - - - + +W1:e--X2:w + + + -W2:w1:e--X3:p1l:w - - - + +W2:e--X3:w + + + -W2:w2:e--X3:p2l:w - - - + +W2:e--X3:w + + + -W2:w3:e--X3:p3l:w - - - + +W2:e--X3:w + + + -W2:w4:e--X3:p4l:w - - - + +W2:e--X3:w + + + -

Bill of Materials

ItemQtyUnitDesignators
Connector, Molex KK 254, female, 4 pins3X1, X2, X3
Wire, 0.25 mm², PK0.4mW1, W2
Wire, 0.25 mm², TQ0.4mW1, W2
Wire, 0.25 mm², VT0.4mW1, W2
Wire, 0.25 mm², YE0.4mW1, W2
\ No newline at end of file +

Bill of Materials

ItemQtyUnitDesignators
Connector, Molex KK 254, female, 4 pins3X1, X2, X3
Wire, 0.25 mm², PK0.4mW1, W2
Wire, 0.25 mm², TQ0.4mW1, W2
Wire, 0.25 mm², VT0.4mW1, W2
Wire, 0.25 mm², YE0.4mW1, W2
\ No newline at end of file diff --git a/examples/ex05.png b/examples/ex05.png index f5c7b3d..c6df384 100644 Binary files a/examples/ex05.png and b/examples/ex05.png differ diff --git a/examples/ex05.svg b/examples/ex05.svg index a147307..943e3a6 100644 --- a/examples/ex05.svg +++ b/examples/ex05.svg @@ -1,270 +1,298 @@ - - - - -%3 - + + + + -X1 - -X1 - -Molex KK 254 - -female - -4-pin - -GND - -VCC - -SCL - -SDA - -1 - -2 - -3 - -4 + +X1 + +X1 + +Molex KK 254 + +female + +4-pin + +GND + +VCC + +SCL + +SDA + +1 + +2 + +3 + +4 -W1 - - -W1 - -4x - -0.25 mm² - -0.2 m -X1:1 -PK -X2:1 - - - -X1:2 -TQ -X2:2 - - - -X1:3 -YE -X2:3 - - - -X1:4 -VT -X2:4 - - - + +W1 + + +W1 + +I2C + +4x + +0.25 mm² + +0.2 m +  +X1:1 +PK +X2:1 + + + +X1:2 +TQ +X2:2 + + + +X1:3 +YE +X2:3 + + + +X1:4 +VT +X2:4 + + + +  -X1:p1r:e--W1:w1:w - - - + +X1:e--W1:w + + + -X1:p2r:e--W1:w2:w - - - + +X1:e--W1:w + + + -X1:p3r:e--W1:w3:w - - - + +X1:e--W1:w + + + -X1:p4r:e--W1:w4:w - - - + +X1:e--W1:w + + + -X2 - -X2 - -Molex KK 254 - -female - -4-pin - -1 - -2 - -3 - -4 - -GND - -VCC - -SCL - -SDA - -1 - -2 - -3 - -4 + +X2 + +X2 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + +GND + +VCC + +SCL + +SDA + +1 + +2 + +3 + +4 -W2 - - -W2 - -4x - -0.25 mm² - -0.2 m -X2:1 -PK -X3:1 - - - -X2:2 -TQ -X3:2 - - - -X2:3 -YE -X3:3 - - - -X2:4 -VT -X3:4 - - - + +W2 + + +W2 + +I2C + +4x + +0.25 mm² + +0.2 m +  +X2:1 +PK +X3:1 + + + +X2:2 +TQ +X3:2 + + + +X2:3 +YE +X3:3 + + + +X2:4 +VT +X3:4 + + + +  -X2:p1r:e--W2:w1:w - - - + +X2:e--W2:w + + + -X2:p2r:e--W2:w2:w - - - + +X2:e--W2:w + + + -X2:p3r:e--W2:w3:w - - - + +X2:e--W2:w + + + -X2:p4r:e--W2:w4:w - - - + +X2:e--W2:w + + + -X3 - -X3 - -Molex KK 254 - -female - -4-pin - -1 - -2 - -3 - -4 - -GND - -VCC - -SCL - -SDA + +X3 + +X3 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + +GND + +VCC + +SCL + +SDA -W1:w1:e--X2:p1l:w - - - + +W1:e--X2:w + + + -W1:w2:e--X2:p2l:w - - - + +W1:e--X2:w + + + -W1:w3:e--X2:p3l:w - - - + +W1:e--X2:w + + + -W1:w4:e--X2:p4l:w - - - + +W1:e--X2:w + + + -W2:w1:e--X3:p1l:w - - - + +W2:e--X3:w + + + -W2:w2:e--X3:p2l:w - - - + +W2:e--X3:w + + + -W2:w3:e--X3:p3l:w - - - + +W2:e--X3:w + + + -W2:w4:e--X3:p4l:w - - - + +W2:e--X3:w + + + diff --git a/examples/ex05.yml b/examples/ex05.yml index 9723b1f..f88815f 100644 --- a/examples/ex05.yml +++ b/examples/ex05.yml @@ -9,6 +9,7 @@ templates: length: 0.2 colors: [PK, TQ, YE, VT] category: bundle + type: I2C connectors: X1: diff --git a/requirements.txt b/requirements.txt index 9c558e3..d24fc95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ . +graphviz +pyyaml diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 6bc9eb8..0e82492 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Optional, List, Any +from typing import Optional, List, Any, Union from dataclasses import dataclass, field from wireviz.wv_helper import int2tuple from wireviz import wv_colors @@ -10,6 +10,9 @@ 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 @@ -21,11 +24,12 @@ class Connector: show_name: bool = True show_pincount: bool = True hide_disconnected_pins: bool = False + autogenerate: bool = False + loops: List[Any] = field(default_factory=list) def __post_init__(self): self.ports_left = False self.ports_right = False - self.loops = [] self.visible_pins = {} if self.pincount is None: @@ -48,11 +52,15 @@ class Connector: 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 + if len(self.pinnumbers) != len(set(self.pinnumbers)): + raise Exception('Pin numbers are not unique') + + for loop in self.loops: + # TODO: check that pins to connect actually exist + # TODO: allow using pin labels in addition to pin numbers, just like when defining regular connections + # TODO: include properties of wire used to create the loop + if len(loop) != 2: + raise Exception('Loops must be between exactly two pins!') def activate_pin(self, pin): self.visible_pins[pin] = True @@ -61,6 +69,9 @@ class Connector: @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 @@ -118,6 +129,16 @@ class Cable: 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) diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 46780cf..faf0376 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -4,9 +4,12 @@ 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 wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, \ + nested_html_table, flatten2d, index_if_list, html_line_breaks, \ + graphviz_line_breaks, remove_line_breaks from collections import Counter from typing import List +from pathlib import Path class Harness: @@ -15,24 +18,47 @@ class Harness: self.color_mode = 'SHORT' self.connectors = {} self.cables = {} + self.additional_bom_items = [] - def add_connector(self, name, *args, **kwargs): + def add_connector(self, name: str, *args, **kwargs) -> None: self.connectors[name] = Connector(name, *args, **kwargs) - def add_cable(self, name, *args, **kwargs): + def add_cable(self, name: str, *args, **kwargs) -> None: 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 add_bom_item(self, item: dict) -> None: + self.additional_bom_items.append(item) + + def connect(self, from_name: str, from_pin: (int, str), via_name: str, via_pin: (int, str), to_name: str, to_pin: (int, str)) -> None: + for (name, pin) in zip([from_name, to_name], [from_pin, to_pin]): # check from and to connectors + if name is not None and name in self.connectors: + connector = self.connectors[name] + if pin in connector.pinnumbers and pin in connector.pinout: + if connector.pinnumbers.index(pin) == connector.pinout.index(pin): + # TODO: Maybe issue a warning? It's not worthy of an exception if it's unambiguous, but maybe risky? + pass + else: + raise Exception(f'{name}:{pin} is defined both in pinout and pinnumbers, for different pins.') + if pin in connector.pinout: + if connector.pinout.count(pin) > 1: + raise Exception(f'{name}:{pin} is defined more than once.') + else: + index = connector.pinout.index(pin) + pin = connector.pinnumbers[index] # map pin name to pin number + if name == from_name: + from_pin = pin + if name == to_name: + to_pin = pin + if not pin in connector.pinnumbers: + raise Exception(f'{name}:{pin} not found.') - 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): + def create_graph(self) -> Graph: dot = Graph() dot.body.append('// Graph generated by WireViz') dot.body.append('// https://github.com/formatc1702/WireViz') @@ -59,43 +85,52 @@ class Harness: 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 '' - colorbar = f'' if connector.color else '' - dot.node(key, shape='none', - style='filled', - margin='0', - orientation='0' if connector.ports_left else '180', - label='''< + rows = [[connector.manufacturer, + f'MPN: {connector.manufacturer_part_number}' if connector.manufacturer_part_number else None, + f'IPN: {connector.internal_part_number}' if connector.internal_part_number else None], + [html_line_breaks(connector.type), html_line_breaks(connector.subtype), wv_colors.translate_color(connector.color, "HEX"), '' if connector.color else None], + [html_line_breaks(connector.notes)]] + html = nested_html_table(rows) - - - {colorbar} - -
{infostring_l} {infostring_r}
+ if connector.color: # add color bar next to color info, if present + colorbar = f' bgcolor="{wv_colors.translate_color(connector.color, "HEX")}" width="4">' # leave out ' tag + html = html.replace('>', colorbar) - - >'''.format(infostring_l=infostring_l, infostring_r=infostring_r, colorbar=colorbar)) + dot.node(key, label=f'<{html}>', shape='none', margin='0', style='filled', fillcolor='white') else: # not a ferrule - attributes = [connector.type, - connector.subtype, - f'{connector.pincount}-pin' if connector.show_pincount else''] - pinouts = [[], [], []] + + rows = [[connector.name if connector.show_name else None], + [connector.manufacturer, + f'MPN: {connector.manufacturer_part_number}' if connector.manufacturer_part_number else None, + f'IPN: {connector.internal_part_number}' if connector.internal_part_number else None], + [html_line_breaks(connector.type), + html_line_breaks(connector.subtype), + f'{connector.pincount}-pin' if connector.show_pincount else None], + '', + [html_line_breaks(connector.notes)]] + html = nested_html_table(rows) + + 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 '', attributes, pinouts, connector.notes] - dot.node(key, label=nested(label)) + pinouts.append([f'{pinnumber}' if connector.ports_left else None, + f'{pinname}' if pinname else '', + f'{pinnumber}' if connector.ports_right else None]) + + pinhtml = '' + for i, pin in enumerate(pinouts): + pinhtml = f'{pinhtml}' + for column in pin: + if column is not None: + pinhtml = f'{pinhtml}{column}' + pinhtml = f'{pinhtml}' + pinhtml = f'{pinhtml}
' + html = html.replace('', pinhtml) + + dot.node(key, label=f'<{html}>', shape='none', margin='0', style='filled', fillcolor='white') if len(connector.loops) > 0: dot.attr('edge', color='#000000:#ffffff:#000000') @@ -123,7 +158,13 @@ class Harness: elif cable.gauge_unit.upper() == 'AWG': awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' - attributes = [f'{len(cable.colors)}x' if cable.show_wirecount else '', + 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 = [html_line_breaks(cable.type) if cable.type else '', + 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 ''] @@ -134,13 +175,20 @@ class Harness: 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}' html = f'{html}' # attribute row html = f'{html}
{cable.name}
' + for attrib in identification[0:-1]: + html = f'{html}' # all columns except last have a border on the right (sides="R") + if len(identification) > 0: + html = f'{html}' # last column has no border on the right because the enclosing table borders it + html = f'{html}
{attrib}{identification[-1]}
{attrib}{attrib}
' # name+attributes table - html = f'{html}' # spacer between attributes and wires + html = f'{html} ' # spacer between attributes and wires html = f'{html}' # conductor table @@ -153,29 +201,50 @@ class Harness: for bla in p: html = f'{html}' html = f'{html}' + #TODO: RESOLVE THIS MESS + 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}' + # -------------------- bgcolors = ('#000000:' + wv_colors.translate_color(connection, 'hex') + ':#000000').split(':') html = f'{html}' + # END OF MESS if cable.shield: p = ['', 'Shield', ''] - html = f'{html}' # spacer + 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}' # spacer at the end html = f'{html}
{bla}
' + for attrib in wireidentification: + html = f'{html}' + html = f'{html}
{attrib}
' for j, bgcolor in enumerate( bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors html = f'{html}' html = html + '
 
{bla}
 
' # conductor table html = f'{html}' # main table if cable.notes: - html = f'{html}{cable.notes}' # notes table - html = f'{html}' # spacer at the end + html = f'{html}{html_line_breaks(cable.notes)}' # notes table + html = f'{html} ' # spacer at the end html = f'{html}' # main table @@ -210,13 +279,31 @@ class Harness: return dot - def output(self, filename, directory='_output', view=False, cleanup=True, fmt='pdf', gen_bom=False): + @property + def png(self): + from io import BytesIO + graph = self.create_graph() + data = BytesIO() + data.write(graph.pipe(format='png')) + data.seek(0) + return data.read() + + @property + def svg(self): + from io import BytesIO + graph = self.create_graph() + data = BytesIO() + data.write(graph.pipe(format='svg')) + data.seek(0) + return data.read() + + def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('pdf', )) -> None: # 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) + graph.render(filename=filename, view=view, cleanup=cleanup) + graph.save(filename=f'{filename}.gv') # bom output bom_list = self.bom_list() with open(f'{filename}.bom.tsv', 'w') as file: @@ -251,80 +338,93 @@ class Harness: bom = [] bom_connectors = [] bom_cables = [] + bom_extra = [] # 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} + connector_group = lambda c: (c.type, c.subtype, c.pincount, c.manufacturer, c.manufacturer_part_number, c.internal_part_number) + for group in Counter([connector_group(v) for v in self.connectors.values()]): + items = {k: v for k, v in self.connectors.items() if connector_group(v) == group} 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_type = f', {remove_line_breaks(shared.type)}' if shared.type else '' + conn_subtype = f', {remove_line_breaks(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 ''} + item = {'item': name, 'qty': len(designators), 'unit': '', 'designators': designators if shared.category != 'ferrule' else '', + 'manufacturer': shared.manufacturer, 'manufacturer part number': shared.manufacturer_part_number, '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) 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} + # TODO: If category can have other non-empty values than 'bundle', maybe it should be part of item name? + # The category needs to be included in cable_group to keep the bundles excluded. + cable_group = lambda c: (c.category, c.type, c.gauge, c.gauge_unit, c.wirecount, c.shield, c.manufacturer, c.manufacturer_part_number, c.internal_part_number) + for group in Counter([cable_group(v) for v in self.cables.values() if v.category != 'bundle']): + items = {k: v for k, v in self.cables.items() if cable_group(v) == group} 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) + designators = list(items.keys()) + designators.sort() + total_length = sum(i.length for i in items.values()) + cable_type = f', {remove_line_breaks(shared.type)}' if shared.type else '' + gauge_name = f' x {shared.gauge} {shared.gauge_unit}' if shared.gauge else ' wires' + shield_name = ' shielded' if shared.shield else '' + name = f'Cable{cable_type}, {shared.wirecount}{gauge_name}{shield_name}' + item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators, + 'manufacturer': shared.manufacturer, 'manufacturer part number': shared.manufacturer_part_number, '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 color in bundle.colors: - wirelist.append({'gauge': shared.gauge, 'gauge_unit': shared.gauge_unit, - 'length': shared.length, 'color': color, 'designator': bundle.name}) + for bundle in self.cables.values(): + if bundle.category == 'bundle': + # add each wire from each bundle to the wirelist + for index, color in enumerate(bundle.colors, 0): + wirelist.append({'type': bundle.type, 'gauge': bundle.gauge, 'gauge_unit': bundle.gauge_unit, 'length': bundle.length, 'color': color, 'designator': bundle.name, + 'manufacturer': index_if_list(bundle.manufacturer, index), + 'manufacturer part number': index_if_list(bundle.manufacturer_part_number, index), + 'internal part number': index_if_list(bundle.internal_part_number, index)}) # 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] + wire_group = lambda w: (w.get('type', None), w['gauge'], w['gauge_unit'], w['color'], w['manufacturer'], w['manufacturer part number'], w['internal part number']) + for group in Counter([wire_group(v) for v in wirelist]): + items = [v for v in wirelist if wire_group(v) == group] shared = items[0] designators = [i['designator'] for i in items] - # remove duplicates - designators = list(dict.fromkeys(designators)) + designators = list(dict.fromkeys(designators)) # remove duplicates 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} + wire_type = f', {remove_line_breaks(shared["type"])}' if shared.get('type', None) else '' + gauge_name = f', {shared["gauge"]} {shared["gauge_unit"]}' if shared.get('gauge', None) else '' + gauge_color = f', {shared["color"]}' if 'color' in shared != '' else '' + name = f'Wire{wire_type}{gauge_name}{gauge_color}' + item = {'item': name, 'qty': round(total_length, 3), 'unit': 'm', 'designators': designators, + 'manufacturer': shared['manufacturer'], 'manufacturer part number': shared['manufacturer part number'], '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_cables = sorted(bom_cables, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050) bom.extend(bom_cables) + + for item in self.additional_bom_items: + name = item['description'] if item.get('description', None) else '' + if isinstance(item.get('designators', None), List): + item['designators'].sort() # sort designators if a list is provided + item = {'item': name, 'qty': item.get('qty', None), 'unit': item.get('unit', None), 'designators': item.get('designators', None), + 'manufacturer': item.get('manufacturer', None), 'manufacturer part number': item.get('manufacturer_part_number', None), 'internal part number': item.get('internal_part_number', None)} + bom_extra.append(item) + bom_extra = sorted(bom_extra, key=lambda k: k['item']) + bom.extend(bom_extra) return bom def bom_list(self): bom = self.bom() - keys = ['item', 'qty', 'unit', 'designators'] + keys = ['item', 'qty', 'unit', 'designators'] # these BOM columns will always be included + for fieldname in ['manufacturer', 'manufacturer part number', 'internal part number']: # these optional BOM columns will only be included if at least one BOM item actually uses them + if any(fieldname in x and x.get(fieldname, None) 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) + item_list = [', '.join(subitem) if isinstance(subitem, List) else subitem for subitem in item_list] # convert any lists into comma separated strings + item_list = ['' if subitem is None else subitem for subitem in item_list] # if a field is missing for some (but not all) BOM items bom_list.append(item_list) return bom_list diff --git a/src/wireviz/build_examples.py b/src/wireviz/build_examples.py index 64f511d..aa77090 100755 --- a/src/wireviz/build_examples.py +++ b/src/wireviz/build_examples.py @@ -9,8 +9,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from wireviz import wireviz demos = 2 # 2 -examples = 10 # 10 -tutorials = 7 # 7 +examples = 10 # 9 +tutorials = 8 # 7 if demos: for i in range(1,demos+1): diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index 81b4771..6c9a186 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -3,54 +3,37 @@ import argparse import os +from pathlib import Path import sys +from typing import Any, Tuple import yaml if __name__ == '__main__': sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + from wireviz.Harness import Harness +from wireviz.wv_helper import expand -def parse(yaml_input, file_out=None, generate_bom=False): +def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, str, Tuple[str]) = None) -> Any: + """ + Parses yaml input string and does the high-level harness conversion + + :param yaml_input: a string containing the yaml input data + :param file_out: + :param return_types: if None, then returns None; if the value is a string, then a + corresponding data format will be returned; if the value is a tuple of strings, + then for every valid format in the `return_types` tuple, another return type + will be generated and returned in the same order; currently supports: + - "png" - will return the PNG data + - "svg" - will return the SVG data + - "harness" - will return the `Harness` instance + """ yaml_data = yaml.safe_load(yaml_input) - def expand(yaml_data): - # yaml_data can be: - # - a singleton (normally str or int) - # - a list of str or int - # 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] - for e in yaml_data: - e = str(e) - if '-' in e: # list of pins - a, b = tuple(map(int, e.split('-'))) - if a < b: - for x in range(a, b + 1): - output.append(x) - elif a > b: - for x in range(a, b - 1, -1): - output.append(x) - elif a == b: - output.append(a) - else: - try: - x = int(e) - except Exception: - x = e - output.append(x) - return output - - def check_designators(what, where): - for i, x in enumerate(what): - if x not in yaml_data[where[i]]: - return False - return True - harness = Harness() # add items @@ -60,11 +43,12 @@ def parse(yaml_input, file_out=None, generate_bom=False): if sec in yaml_data and type(yaml_data[sec]) == ty: if len(yaml_data[sec]) > 0: if ty == dict: - for key, o in yaml_data[sec].items(): + for key, attribs in yaml_data[sec].items(): if sec == 'connectors': - harness.add_connector(name=key, **o) + if not attribs.get('autogenerate', False): + harness.add_connector(name=key, **attribs) elif sec == 'cables': - harness.add_cable(name=key, **o) + harness.add_cable(name=key, **attribs) elif sec == 'ferrules': pass else: @@ -76,113 +60,144 @@ def parse(yaml_input, file_out=None, generate_bom=False): yaml_data[sec] = [] # add connections - ferrule_counter = 0 - for connections in yaml_data['connections']: - if len(connections) == 3: # format: connector -- cable -- connector - 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') + def check_designators(what, where): # helper function + for i, x in enumerate(what): + if x not in yaml_data[where[i]]: + return False + return True - 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]) - raise Exception('Bad connection definition (3)') - - 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): - harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) - - elif len(connections) == 2: - - 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(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(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')) - - 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(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): - raise Exception('List length mismatch') - - if con_cbl or cbl_con: - for (from_pin, to_pin) in zip(from_pins, to_pins): - if con_cbl: - 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(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): - harness.loop(cocon_coname, from_pin, to_pin) - if fer_cbl or cbl_fer: - from_pins = expand(connections[0][from_name]) - to_pins = expand(connections[1][to_name]) - - if fer_cbl: - ferrule_name = from_name - cable_name = to_name - cable_pins = to_pins - else: - ferrule_name = to_name - cable_name = from_name - cable_pins = from_pins - - ferrule_params = yaml_data['ferrules'][ferrule_name] - for cable_pin in cable_pins: - ferrule_counter = ferrule_counter + 1 - ferrule_id = f'_F{ferrule_counter}' - harness.add_connector(ferrule_id, category='ferrule', **ferrule_params) - - if fer_cbl: - harness.connect(ferrule_id, 1, cable_name, cable_pin, None, None) - else: - harness.connect(None, None, cable_name, cable_pin, ferrule_id, 1) + autogenerated_ids = {} + for connection in yaml_data['connections']: + # find first component (potentially nested inside list or dict) + first_item = connection[0] + if isinstance(first_item, list): + first_item = first_item[0] + elif isinstance(first_item, dict): + first_item = list(first_item.keys())[0] + elif isinstance(first_item, str): + pass + # check which section the first item belongs to + alternating_sections = ['connectors','cables'] + for index, section in enumerate(alternating_sections): + if first_item in yaml_data[section]: + expected_index = index + break else: - raise Exception('Wrong number of connection parameters') + raise Exception('First item not found anywhere.') + expected_index = 1 - expected_index # flip once since it is flipped back at the *beginning* of every loop - harness.output(filename=file_out, fmt=('png', 'svg'), gen_bom=generate_bom, view=False) + # check that all iterable items (lists and dicts) are the same length + # and that they are alternating between connectors and cables/bundles, starting with either + itemcount = None + for item in connection: + expected_index = 1 - expected_index # make sure items alternate between connectors and cables + expected_section = alternating_sections[expected_index] + if isinstance(item, list): + itemcount_new = len(item) + for subitem in item: + if not subitem in yaml_data[expected_section]: + raise Exception(f'{subitem} is not in {expected_section}') + elif isinstance(item, dict): + if len(item.keys()) != 1: + raise Exception('Dicts may contain only one key here!') + itemcount_new = len(expand(list(item.values())[0])) + subitem = list(item.keys())[0] + if not subitem in yaml_data[expected_section]: + raise Exception(f'{subitem} is not in {expected_section}') + elif isinstance(item, str): + if not item in yaml_data[expected_section]: + raise Exception(f'{item} is not in {expected_section}') + continue + if itemcount is not None and itemcount_new != itemcount: + raise Exception('All lists and dict lists must be the same length!') + itemcount = itemcount_new + if itemcount is None: + raise Exception('No item revealed the number of connections to make!') + + # populate connection list + connection_list = [] + for i, item in enumerate(connection): + if isinstance(item, str): # one single-pin component was specified + sublist = [] + for i in range(1, itemcount + 1): + if yaml_data['connectors'][item].get('autogenerate'): + autogenerated_ids[item] = autogenerated_ids.get(item, 0) + 1 + new_id = f'_{item}_{autogenerated_ids[item]}' + harness.add_connector(new_id, **yaml_data['connectors'][item]) + sublist.append([new_id, 1]) + else: + sublist.append([item, 1]) + connection_list.append(sublist) + elif isinstance(item, list): # a list of single-pin components were specified + sublist = [] + for subitem in item: + if yaml_data['connectors'][subitem].get('autogenerate'): + autogenerated_ids[subitem] = autogenerated_ids.get(subitem, 0) + 1 + new_id = f'_{subitem}_{autogenerated_ids[subitem]}' + harness.add_connector(new_id, **yaml_data['connectors'][subitem]) + sublist.append([new_id, 1]) + else: + sublist.append([subitem, 1]) + connection_list.append(sublist) + elif isinstance(item, dict): # a component with multiple pins was specified + sublist = [] + id = list(item.keys())[0] + pins = expand(list(item.values())[0]) + for pin in pins: + sublist.append([id, pin]) + connection_list.append(sublist) + else: + raise Exception('Unexpected item in connection list') + + # actually connect components using connection list + for i, item in enumerate(connection_list): + id = item[0][0] # TODO: make more elegant/robust/pythonic + if id in harness.cables: + for j, con in enumerate(item): + if i == 0: # list started with a cable, no connector to join on left side + from_name = None + from_pin = None + else: + from_name = connection_list[i-1][j][0] + from_pin = connection_list[i-1][j][1] + via_name = item[j][0] + via_pin = item[j][1] + if i == len(connection_list) - 1: # list ends with a cable, no connector to join on right side + to_name = None + to_pin = None + else: + to_name = connection_list[i+1][j][0] + to_pin = connection_list[i+1][j][1] + harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) + + if "additional_bom_items" in yaml_data: + for line in yaml_data["additional_bom_items"]: + harness.add_bom_item(line) + + if file_out is not None: + harness.output(filename=file_out, fmt=('png', 'svg'), view=False) + + if return_types is not None: + returns = [] + if isinstance(return_types, str): # only one return type speficied + return_types = [return_types] + + return_types = [t.lower() for t in return_types] + + for rt in return_types: + if rt == 'png': + returns.append(harness.png) + if rt == 'svg': + returns.append(harness.svg) + if rt == 'harness': + returns.append(harness) + + return tuple(returns) if len(returns) != 1 else returns[0] -def parse_file(yaml_file, file_out=None, generate_bom=False): +def parse_file(yaml_file: str, file_out: (str, Path) = None) -> None: with open(yaml_file, 'r') as file: yaml_input = file.read() @@ -191,7 +206,7 @@ def parse_file(yaml_file, file_out=None, generate_bom=False): file_out = fn file_out = os.path.abspath(file_out) - parse(yaml_input, file_out=file_out, generate_bom=generate_bom) + parse(yaml_input, file_out=file_out) def parse_cmdline(): @@ -232,7 +247,7 @@ def main(): file_out = args.output_file 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) if __name__ == '__main__': diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 222fd74..016fe96 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -30,19 +30,52 @@ def awg_equiv(mm2): def mm2_equiv(awg): return mm2_equiv_table.get(str(awg), 'Unknown') -def nested(inp): - l = [] - for x in inp: - if isinstance(x, list): - if len(x) > 0: - n = nested(x) - if n != '': - l.append('{' + n + '}') +def nested_html_table(rows): + # input: list, each item may be scalar or list + # output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar + # purpose: create the appearance of one table, where cell widths are independent between rows + html = '' + for row in rows: + if isinstance(row, List): + if len(row) > 0 and any(row): + html = f'{html}' + elif row is not None: + html = f'{html}' + html = f'{html}
' + for cell in row: + if cell is not None: + html = f'{html}' + html = f'{html}
{cell}
{row}
' + return html + + +def expand(yaml_data): + # yaml_data can be: + # - a singleton (normally str or int) + # - a list of str or int + # 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] + for e in yaml_data: + e = str(e) + if '-' in e: # list of pins + a, b = tuple(map(int, e.split('-'))) + if a < b: + for x in range(a, b + 1): + output.append(x) + elif a > b: + for x in range(a, b - 1, -1): + output.append(x) + elif a == b: + output.append(a) else: - if x is not None: - if x != '': - l.append(str(x)) - return '|'.join(l) + try: + x = int(e) + except Exception: + x = e + output.append(x) + return output def int2tuple(inp): @@ -65,3 +98,16 @@ def tuplelist2tsv(inp, header=None): for row in inp: output = output + '\t'.join(str(item) for item in row) + '\n' return output + +# Return the value indexed if it is a list, or simply the value otherwise. +def index_if_list(value, index): + return value[index] if isinstance(value, list) else value + +def html_line_breaks(inp): + return inp.replace('\n', '
') if isinstance(inp, str) else inp + +def graphviz_line_breaks(inp): + return inp.replace('\n', '\\n') if isinstance(inp, str) else inp # \n generates centered new lines. http://www.graphviz.org/doc/info/attrs.html#k:escString + +def remove_line_breaks(inp): + return inp.replace('\n', ' ').rstrip() if isinstance(inp, str) else inp diff --git a/tutorial/readme.md b/tutorial/readme.md index dc7bbcb..b82d7cb 100644 --- a/tutorial/readme.md +++ b/tutorial/readme.md @@ -203,9 +203,9 @@ connectors: pinout: [+12V, GND, GND, +5V] type: Molex 8981 subtype: female - -ferrules: # ferrules F1: + category: ferrule + autogenerate: true type: Ferrule, crimp subtype: 0.5 mm² color: OG # optional color @@ -253,9 +253,9 @@ connectors: type: Ferrule, crimp subtype: 1.0 mm² color: YE - -ferrules: # ferrules F_05: + category: ferrule + autogenerate: true type: Ferrule, crimp subtype: 0.5 mm² color: OG # optional color @@ -361,3 +361,63 @@ Output: [Bill of Materials](tutorial07.bom.tsv) +## 8 - Part numbers + +* Part number information can be added to parts + * Only provided fields will be added to the diagram and bom +* Bundles can have part information specified by wire + +[Source](tutorial08.yml): + +```yaml +connectors: + X1: &template1 # define a template for later use + type: Molex KK 254 + pincount: 4 + subtype: female + manufacturer: Molex + manufacturer_part_number: 22013047 + X2: + <<: *template1 # reuse template + internal_part_number: CON4 + X3: + <<: *template1 # reuse template + +cables: + W1: + wirecount: 4 + length: 1 + gauge: 0.25 mm2 + color_code: IEC + manufacturer: CablesCo + manufacturer_part_number: ABC123 + internal_part_number: CAB1 + W2: + category: bundle + length: 1 + gauge: 0.25 mm2 + colors: [YE, BK, BK, RD] + manufacturer: [WiresCo,WiresCo,WiresCo,WiresCo] + manufacturer_part_number: [W1-YE,W1-BK,W1-BK,W1-RD] + internal_part_number: [WIRE1,WIRE2,WIRE2,WIRE3] + + +connections: + - + - X1: [1-4] + - W1: [1-4] + - X2: [1-4] + - + - X1: [1-4] + - W2: [1-4] + - X3: [1-4] +``` + + +Output: + +![](tutorial08.png) + +[Bill of Materials](tutorial08.bom.tsv) + + diff --git a/tutorial/tutorial05.yml b/tutorial/tutorial05.yml index e238f30..dda905e 100644 --- a/tutorial/tutorial05.yml +++ b/tutorial/tutorial05.yml @@ -3,9 +3,9 @@ connectors: pinout: [+12V, GND, GND, +5V] type: Molex 8981 subtype: female - -ferrules: # ferrules F1: + category: ferrule + autogenerate: true type: Ferrule, crimp subtype: 0.5 mm² color: OG # optional color diff --git a/tutorial/tutorial06.yml b/tutorial/tutorial06.yml index f69499c..bcf9c8c 100644 --- a/tutorial/tutorial06.yml +++ b/tutorial/tutorial06.yml @@ -8,9 +8,9 @@ connectors: type: Ferrule, crimp subtype: 1.0 mm² color: YE - -ferrules: # ferrules F_05: + category: ferrule + autogenerate: true type: Ferrule, crimp subtype: 0.5 mm² color: OG # optional color diff --git a/tutorial/tutorial08.bom.tsv b/tutorial/tutorial08.bom.tsv new file mode 100644 index 0000000..682dd4a --- /dev/null +++ b/tutorial/tutorial08.bom.tsv @@ -0,0 +1,7 @@ +Item Qty Unit Designators Manufacturer Manufacturer part number Internal part number +Connector, Molex KK 254, female, 4 pins 2 X1, X3 Molex 22013047 +Connector, Molex KK 254, female, 4 pins 1 X2 Molex 22013047 CON4 +Cable, 4 x 0.25 mm² 1 m W1 CablesCo ABC123 CAB1 +Wire, 0.25 mm², BK 2 m W2 WiresCo W1-BK WIRE2 +Wire, 0.25 mm², RD 1 m W2 WiresCo W1-RD WIRE3 +Wire, 0.25 mm², YE 1 m W2 WiresCo W1-YE WIRE1 diff --git a/tutorial/tutorial08.gv b/tutorial/tutorial08.gv new file mode 100644 index 0000000..1bbf045 --- /dev/null +++ b/tutorial/tutorial08.gv @@ -0,0 +1,36 @@ +graph { +// Graph generated by WireViz +// https://github.com/formatc1702/WireViz + 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] + X1 [label="X1|{Molex|MPN: 22013047}|{Molex KK 254|female|4-pin}|{{1|2|3|4}}"] + X2 [label="X2|{Molex|MPN: 22013047|IPN: CON4}|{Molex KK 254|female|4-pin}|{{1|2|3|4}}"] + X3 [label="X3|{Molex|MPN: 22013047}|{Molex KK 254|female|4-pin}|{{1|2|3|4}}"] + edge [color="#000000:#666600:#000000"] + X1:p1r:e -- W1:w1:w + W1:w1:e -- X2:p1l:w + edge [color="#000000:#ff0000:#000000"] + X1:p2r:e -- W1:w2:w + W1:w2:e -- X2:p2l:w + edge [color="#000000:#ff8000:#000000"] + X1:p3r:e -- W1:w3:w + W1:w3:e -- X2:p3l:w + edge [color="#000000:#ffff00:#000000"] + X1:p4r:e -- W1:w4:w + W1:w4:e -- X2:p4l:w + W1 [label=<
W1
CablesCoMPN: ABC123IPN: CAB1
4x0.25 mm²1 m
 
X1:1BNX2:1
X1:2RDX2:2
X1:3OGX2:3
X1:4YEX2:4
 
> fillcolor=white margin=0 shape=box style=""] + edge [color="#000000:#ffff00:#000000"] + X1:p1r:e -- W2:w1:w + W2:w1:e -- X3:p1l:w + edge [color="#000000:#000000:#000000"] + X1:p2r:e -- W2:w2:w + W2:w2:e -- X3:p2l:w + edge [color="#000000:#000000:#000000"] + X1:p3r:e -- W2:w3:w + W2:w3:e -- X3:p3l:w + edge [color="#000000:#ff0000:#000000"] + X1:p4r:e -- W2:w4:w + W2:w4:e -- X3:p4l:w + W2 [label=<
W2
4x0.25 mm²1 m
 
X1:1YEX3:1
WiresCoMPN: W1-YEIPN: WIRE1
X1:2BKX3:2
WiresCoMPN: W1-BKIPN: WIRE2
X1:3BKX3:3
WiresCoMPN: W1-BKIPN: WIRE2
X1:4RDX3:4
WiresCoMPN: W1-RDIPN: WIRE3
 
> fillcolor=white margin=0 shape=box style="filled,dashed"] +} diff --git a/tutorial/tutorial08.html b/tutorial/tutorial08.html new file mode 100644 index 0000000..a031af8 --- /dev/null +++ b/tutorial/tutorial08.html @@ -0,0 +1,295 @@ +

Diagram

+ + + + + + + + +X1 + +X1 + +Molex + +MPN: 22013047 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + + + +W1 + + +W1 + + +CablesCo + +MPN: ABC123 +IPN: CAB1 + +4x + +0.25 mm² + +1 m +  +X1:1 +BN +X2:1 + + + +X1:2 +RD +X2:2 + + + +X1:3 +OG +X2:3 + + + +X1:4 +YE +X2:4 + + + +  + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +W2 + + +W2 + +4x + +0.25 mm² + +1 m +  +X1:1 +YE +X3:1 + + + +WiresCo +MPN: W1-YE +IPN: WIRE1 +X1:2 +BK +X3:2 + + + +WiresCo +MPN: W1-BK +IPN: WIRE2 +X1:3 +BK +X3:3 + + + +WiresCo +MPN: W1-BK +IPN: WIRE2 +X1:4 +RD +X3:4 + + + +WiresCo +MPN: W1-RD +IPN: WIRE3 +  + + + +X1:e--W2:w + + + + + + +X1:e--W2:w + + + + + + +X1:e--W2:w + + + + + + +X1:e--W2:w + + + + + + +X2 + +X2 + +Molex + +MPN: 22013047 + +IPN: CON4 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + + + +X3 + +X3 + +Molex + +MPN: 22013047 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + +W2:e--X3:w + + + + + + +W2:e--X3:w + + + + + + +W2:e--X3:w + + + + + + +W2:e--X3:w + + + + + + +

Bill of Materials

ItemQtyUnitDesignatorsManufacturerManufacturer part numberInternal part number
Connector, Molex KK 254, female, 4 pins2X1, X3Molex22013047
Connector, Molex KK 254, female, 4 pins1X2Molex22013047CON4
Cable, 4 x 0.25 mm²1mW1CablesCoABC123CAB1
Wire, 0.25 mm², BK2mW2WiresCoW1-BKWIRE2
Wire, 0.25 mm², RD1mW2WiresCoW1-RDWIRE3
Wire, 0.25 mm², YE1mW2WiresCoW1-YEWIRE1
\ No newline at end of file diff --git a/tutorial/tutorial08.md b/tutorial/tutorial08.md new file mode 100644 index 0000000..8369e90 --- /dev/null +++ b/tutorial/tutorial08.md @@ -0,0 +1,5 @@ +## Part numbers + +* Part number information can be added to parts + * Only provided fields will be added to the diagram and bom +* Bundles can have part information specified by wire diff --git a/tutorial/tutorial08.png b/tutorial/tutorial08.png new file mode 100644 index 0000000..76a2ff6 Binary files /dev/null and b/tutorial/tutorial08.png differ diff --git a/tutorial/tutorial08.svg b/tutorial/tutorial08.svg new file mode 100644 index 0000000..9fc5336 --- /dev/null +++ b/tutorial/tutorial08.svg @@ -0,0 +1,294 @@ + + + + + + + + + +X1 + +X1 + +Molex + +MPN: 22013047 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + + + +W1 + + +W1 + + +CablesCo + +MPN: ABC123 +IPN: CAB1 + +4x + +0.25 mm² + +1 m +  +X1:1 +BN +X2:1 + + + +X1:2 +RD +X2:2 + + + +X1:3 +OG +X2:3 + + + +X1:4 +YE +X2:4 + + + +  + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +X1:e--W1:w + + + + + + +W2 + + +W2 + +4x + +0.25 mm² + +1 m +  +X1:1 +YE +X3:1 + + + +WiresCo +MPN: W1-YE +IPN: WIRE1 +X1:2 +BK +X3:2 + + + +WiresCo +MPN: W1-BK +IPN: WIRE2 +X1:3 +BK +X3:3 + + + +WiresCo +MPN: W1-BK +IPN: WIRE2 +X1:4 +RD +X3:4 + + + +WiresCo +MPN: W1-RD +IPN: WIRE3 +  + + + +X1:e--W2:w + + + + + + +X1:e--W2:w + + + + + + +X1:e--W2:w + + + + + + +X1:e--W2:w + + + + + + +X2 + +X2 + +Molex + +MPN: 22013047 + +IPN: CON4 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + + + +X3 + +X3 + +Molex + +MPN: 22013047 + +Molex KK 254 + +female + +4-pin + +1 + +2 + +3 + +4 + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + +W1:e--X2:w + + + + + + +W2:e--X3:w + + + + + + +W2:e--X3:w + + + + + + +W2:e--X3:w + + + + + + +W2:e--X3:w + + + + + + diff --git a/tutorial/tutorial08.yml b/tutorial/tutorial08.yml new file mode 100644 index 0000000..56a3800 --- /dev/null +++ b/tutorial/tutorial08.yml @@ -0,0 +1,41 @@ +connectors: + X1: &template1 # define a template for later use + type: Molex KK 254 + pincount: 4 + subtype: female + manufacturer: Molex + manufacturer_part_number: 22013047 + X2: + <<: *template1 # reuse template + internal_part_number: CON4 + X3: + <<: *template1 # reuse template + +cables: + W1: + wirecount: 4 + length: 1 + gauge: 0.25 mm2 + color_code: IEC + manufacturer: CablesCo + manufacturer_part_number: ABC123 + internal_part_number: CAB1 + W2: + category: bundle + length: 1 + gauge: 0.25 mm2 + colors: [YE, BK, BK, RD] + manufacturer: [WiresCo,WiresCo,WiresCo,WiresCo] + manufacturer_part_number: [W1-YE,W1-BK,W1-BK,W1-RD] + internal_part_number: [WIRE1,WIRE2,WIRE2,WIRE3] + + +connections: + - + - X1: [1-4] + - W1: [1-4] + - X2: [1-4] + - + - X1: [1-4] + - W2: [1-4] + - X3: [1-4]