Compare commits

...

9 Commits

Author SHA1 Message Date
Daniel Rojas
1799490bf2 Test additional component table (WIP) 2021-10-15 14:04:44 +02:00
Daniel Rojas
26b505120a Add option to hide BOM IDs 2021-10-15 09:50:54 +02:00
Daniel Rojas
262ea42caf Implement parent classes
- `Component`
- `GraphicalComponent`
- `TopLevelGraphicalComponent`
2021-10-15 09:50:39 +02:00
Daniel Rojas
d1d7a7ced8 Change namedtuples' typenames to match object type 2021-10-15 09:10:15 +02:00
Daniel Rojas
c3d8666467 Edit tutorial08.yml for compatibility with refactored code 2021-10-14 22:44:19 +02:00
Daniel Rojas
be307e4917 Assign BOM IDs to components before generating graphical output 2021-10-14 22:30:18 +02:00
Daniel Rojas
a39104b51c Populate Harness.bom during creation of components
Additionally: harmonize the `additional_components` inside Connectors/Cables with the `additional_bom_items` from the YAML
2021-10-14 21:27:38 +02:00
Daniel Rojas
884dca8a88 Implement .description and .bom_hash for all components
Implemented for connectors, cables, bundles, additional components
2021-10-14 20:19:44 +02:00
Daniel Rojas
e8fc1d2212 Disable all calls to wv_bom.py and rename it to wv_bom_old.py
to ensure no legacy code is called.
2021-10-14 19:46:31 +02:00
8 changed files with 282 additions and 73 deletions

View File

@ -6,7 +6,8 @@ from pathlib import Path
from wireviz.wv_helper import int2tuple, aspect_ratio
from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES
from wireviz.wv_bom_new import Bom_hash, Bom_hash_list
from wireviz.wv_gv_html import nested_html_table, bom_bubble
# Each type alias have their legal values described in comments - validation might be implemented in the future
PlainText = str # Text not containing HTML tags nor newlines
@ -41,6 +42,7 @@ class Options:
bgcolor_cable: Optional[Color] = None
bgcolor_bundle: Optional[Color] = None
color_mode: ColorMode = 'SHORT'
show_bom_ids: bool = False
mini_bom_mode: bool = True
def __post_init__(self):
@ -98,18 +100,70 @@ class Image:
@dataclass
class AdditionalComponent:
type: MultilineHypertext
subtype: Optional[MultilineHypertext] = None
manufacturer: Optional[MultilineHypertext] = None
mpn: Optional[MultilineHypertext] = None
supplier: Optional[MultilineHypertext] = None
spn: Optional[MultilineHypertext] = None
pn: Optional[Hypertext] = None
class Component:
type: Union[MultilineHypertext, List[MultilineHypertext]] = None
subtype: Union[MultilineHypertext, List[MultilineHypertext]] = None
category: Optional[str] = None # currently only used by cables, to define bundles
pn: Union[Hypertext, List[Hypertext], None] = None
manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
spn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
ignore_in_bom: bool = False
bom_id: Optional[str] = None # to be filled after harness is built
@property
def bom_hash(self) -> Bom_hash:
def force_list(inp):
if isinstance(inp, list):
return inp
else:
return [inp for i in range(len(self.colors))]
if self.category == 'bundle':
# create a single item that includes the necessary fields,
# which may or may not be lists
_hash_list = Bom_hash_list(
self.description,
self.unit,
self.pn,
self.manufacturer,
self.mpn,
self.supplier,
self.spn,
)
# convert elements that are not lists, into lists
_hash_matrix = list(map(force_list, [elem for elem in _hash_list]))
# transpose list of lists, convert to tuple for next step
_hash_matrix = list(map(tuple, zip(*_hash_matrix)))
# generate list of Bom_hashes
hash_list = [Bom_hash(*item) for item in _hash_matrix]
return hash_list
else:
return Bom_hash(
self.description,
self.unit,
self.pn,
self.manufacturer,
self.mpn,
self.supplier,
self.spn,
)
@dataclass
class GraphicalComponent(Component):
bgcolor: Optional[Color] = None
@dataclass
class AdditionalComponent(Component):
qty: float = 1
unit: Optional[str] = None
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
bgcolor: Optional[Color] = None
designators: Optional[str] = None # used for components define in the `additional_bom_items` YAML section
@property
def description(self) -> str:
@ -117,33 +171,38 @@ class AdditionalComponent:
@dataclass
class Connector:
name: Designator
bgcolor: Optional[Color] = None
class TopLevelGraphicalComponent(GraphicalComponent):
name: Designator = None
bgcolor_title: Optional[Color] = None
manufacturer: Optional[MultilineHypertext] = None
mpn: Optional[MultilineHypertext] = None
supplier: Optional[MultilineHypertext] = None
spn: Optional[MultilineHypertext] = None
pn: Optional[Hypertext] = None
style: Optional[str] = None
category: Optional[str] = None
type: Optional[MultilineHypertext] = None
subtype: Optional[MultilineHypertext] = None
pincount: Optional[int] = None
color: Optional[Color] = None
image: Optional[Image] = None
notes: Optional[MultilineHypertext] = None
additional_components: List[AdditionalComponent] = field(default_factory=list)
show_name: bool = True
def gen_add_bom_table(self):
if self.additional_components:
rows = []
for comp in self.additional_components:
rows.append([bom_bubble(comp.bom_id), comp.qty, comp.description, comp.pn])
return rows
else:
return None
@dataclass
class Connector(TopLevelGraphicalComponent):
style: Optional[str] = None
pincount: Optional[int] = None
pins: List[Pin] = field(default_factory=list)
pinlabels: List[Pin] = field(default_factory=list)
pincolors: List[Color] = field(default_factory=list)
color: Optional[Color] = None
show_name: Optional[bool] = None
show_pincount: Optional[bool] = None
hide_disconnected_pins: bool = False
autogenerate: bool = False
loops: List[List[Pin]] = field(default_factory=list)
ignore_in_bom: bool = False
additional_components: List[AdditionalComponent] = field(default_factory=list)
unit = None
def __post_init__(self) -> None:
@ -188,10 +247,11 @@ class Connector:
if isinstance(item, dict):
self.additional_components[i] = AdditionalComponent(**item)
def activate_pin(self, pin: Pin) -> None:
self.visible_pins[pin] = True
def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int:
def qty_factor(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int:
if not qty_multiplier:
return 1
elif qty_multiplier == 'pincount':
@ -201,37 +261,32 @@ class Connector:
else:
raise ValueError(f'invalid qty multiplier parameter for connector {qty_multiplier}')
@property
def description(self) -> str:
substrs = [
'Connector',
self.type,
self.subtype,
self.pincount if self.show_pincount else None,
str(self.color) + ' (reimplement color translation!)' if self.color else None, # translate_color(self.color, harness.options.color_mode)] <- get harness.color_mode!
]
return ', '.join([str(s) for s in substrs if s is not None and s != ''])
@dataclass
class Cable:
name: Designator
bgcolor: Optional[Color] = None
bgcolor_title: Optional[Color] = None
manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
spn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
pn: Union[Hypertext, List[Hypertext], None] = None
category: Optional[str] = None
type: Optional[MultilineHypertext] = None
class Cable(TopLevelGraphicalComponent):
gauge: Optional[float] = None
gauge_unit: Optional[str] = None
show_equiv: bool = False
length: float = 0
length_unit: Optional[str] = None
color: Optional[Color] = None
wirecount: Optional[int] = None
shield: Union[bool, Color] = False
image: Optional[Image] = None
notes: Optional[MultilineHypertext] = None
colors: List[Colors] = field(default_factory=list)
wirelabels: List[Wire] = field(default_factory=list)
color_code: Optional[ColorScheme] = None
show_name: bool = True
show_wirecount: bool = True
show_wirenumbers: Optional[bool] = None
ignore_in_bom: bool = False
additional_components: List[AdditionalComponent] = field(default_factory=list)
def __post_init__(self) -> None:
@ -318,6 +373,7 @@ class Cable:
if isinstance(item, dict):
self.additional_components[i] = AdditionalComponent(**item)
# The *_pin arguments accept a tuple, but it seems not in use with the current code.
def connect(self, from_name: Optional[Designator], from_pin: NoneOrMorePinIndices, via_wire: OneOrMoreWires,
to_name: Optional[Designator], to_pin: NoneOrMorePinIndices) -> None:
@ -329,7 +385,7 @@ class Cable:
for i, _ in enumerate(from_pin):
self.connections.append(Connection(from_name, from_pin[i], via_wire[i], to_name, to_pin[i]))
def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float:
def qty_factor(self, qty_multiplier: Optional[CableMultiplier]) -> float:
if not qty_multiplier:
return 1
elif qty_multiplier == 'wirecount':
@ -343,6 +399,37 @@ class Cable:
else:
raise ValueError(f'invalid qty multiplier parameter for cable {qty_multiplier}')
@property
def unit(self): # for compatibility with parent class
return self.length_unit
@property
def description(self) -> str:
if self.category == 'bundle':
desc_list = []
for index, color in enumerate(self.colors):
substrs = [
'Wire',
self.type,
self.subtype,
f'{self.gauge} {self.gauge_unit}' if self.gauge else None,
str(self.color) + ' (reimplement color translation!)' if self.color else None, # translate_color(self.color, harness.options.color_mode)] <- get harness.color_mode!
]
desc_list.append(', '.join([s for s in substrs if s is not None and s != '']))
return desc_list
else:
substrs = [
('', 'Cable'),
(', ', self.type),
(', ', self.subtype),
(', ', self.wirecount),
(' ', f'x {self.gauge} {self.gauge_unit}' if self.gauge else ' wires'),
(' ', 'shielded' if self.shield else None),
(', ', str(self.color) + ' (reimplement color translation!)' if self.color else None)
]
desc = ''.join([f'{s[0]}{s[1]}' for s in substrs if s[1] is not None and s[1] != ''])
return desc
@dataclass
class Connection:

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from graphviz import Graph
from collections import Counter
from collections import Counter, defaultdict
from typing import Any, List, Union
from dataclasses import dataclass
from pathlib import Path
@ -9,17 +9,18 @@ from itertools import zip_longest
import re
from wireviz import wv_colors, __version__, APP_NAME, APP_URL
from wireviz.DataClasses import Metadata, Options, Tweak, Connector, Cable
from wireviz.DataClasses import AdditionalComponent, Metadata, Options, Tweak, Connector, Cable
from wireviz.wv_colors import get_color_hex, translate_color
from wireviz.wv_gv_html import nested_html_table, \
html_bgcolor_attr, html_bgcolor, html_colorbar, \
html_image, html_caption, remove_links, html_line_breaks
from wireviz.wv_bom import pn_info_string, component_table_entry, \
get_additional_component_table, bom_list, generate_bom, \
HEADER_PN, HEADER_MPN, HEADER_SPN
html_image, html_caption, remove_links, html_line_breaks, bom_bubble, html_table
# from wireviz.wv_bom import pn_info_string, component_table_entry, \
# get_additional_component_table, bom_list, generate_bom, \
# HEADER_PN, HEADER_MPN, HEADER_SPN
from wireviz.wv_bom_new import pn_info_string, HEADER_PN, HEADER_MPN, HEADER_SPN
from wireviz.wv_html import generate_html_output
from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
open_file_read, open_file_write
open_file_read, open_file_write, remove_empty_columns
@dataclass
@ -31,17 +32,68 @@ class Harness:
def __post_init__(self):
self.connectors = {}
self.cables = {}
self._bom = [] # Internal Cache for generated bom
# self.bom = defaultdict(lambda: defaultdict(list)) # https://stackoverflow.com/questions/19189274
self.bom = defaultdict(dict)
self.additional_bom_items = []
def add_connector(self, name: str, *args, **kwargs) -> None:
self.connectors[name] = Connector(name, *args, **kwargs)
self.connectors[name] = Connector(name=name, *args, **kwargs)
self._add_to_internal_bom(self.connectors[name])
def add_cable(self, name: str, *args, **kwargs) -> None:
self.cables[name] = Cable(name, *args, **kwargs)
self.cables[name] = Cable(name=name, *args, **kwargs)
self._add_to_internal_bom(self.cables[name])
def add_bom_item(self, item: dict) -> None:
self.additional_bom_items.append(item)
def add_additional_bom_item(self, item: dict) -> None:
new_item = AdditionalComponent(**item)
self.additional_bom_items.append(new_item)
self._add_to_internal_bom(new_item)
def _add_to_internal_bom(self, item):
if item.ignore_in_bom:
return
def _add(thing, designator=None, qty=1, category=None):
# generate entry
bom_entry = self.bom[thing]
# initialize missing fields
if not 'qty' in bom_entry:
bom_entry['qty'] = 0
if not 'designators' in bom_entry:
bom_entry['designators'] = set()
# update fields
bom_entry['qty'] += qty
if designator:
if isinstance(designator, str):
bom_entry['designators'].add(designator)
else:
bom_entry['designators'].update(designator)
bom_entry['category'] = category
if isinstance(item, Connector):
_add(item.bom_hash, designator=item.name, category='connector')
for comp in item.additional_components:
if comp.ignore_in_bom:
continue
_add(comp.bom_hash, designator=item.name, qty=comp.qty, category='connector/additional')
elif isinstance(item, Cable):
_bom_hash = item.bom_hash
if isinstance(_bom_hash, list):
_cat = 'bundle'
for subhash in _bom_hash:
_add(subhash, designator=item.name, category=_cat)
else:
_cat = 'cable'
_add(item.bom_hash, designator=item.name, category=_cat)
for comp in item.additional_components:
if comp.ignore_in_bom:
continue
_add(comp.bom_hash, designator=item.name, qty=comp.qty, category=f'{_cat}/additional')
elif isinstance(item, AdditionalComponent): # additional component
_add(item.bom_hash, designator=item.designators, qty=item.qty, category='additional')
else:
raise Exception(f'Unknown type of item:\n{item}')
def connect(self, from_name: str, from_pin: (int, str), via_name: str, via_wire: (int, str), to_name: str, to_pin: (int, str)) -> None:
# check from and to connectors
@ -119,6 +171,8 @@ class Harness:
for connector in self.connectors.values():
connector.bom_id = self.bom[connector.bom_hash]['bom_id']
# If no wires connected (except maybe loop wires)?
if not (connector.ports_left or connector.ports_right):
connector.ports_left = True # Use left side pins.
@ -127,7 +181,8 @@ class Harness:
rows = [[f'{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}'
if connector.show_name else None],
[pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
[bom_bubble(connector.bom_id) if self.options.show_bom_ids else None,
pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)),
html_line_breaks(pn_info_string(HEADER_SPN, connector.supplier, connector.spn))],
[html_line_breaks(connector.type),
@ -137,8 +192,10 @@ class Harness:
html_colorbar(connector.color)],
'<!-- connector table -->' if connector.style != 'simple' else None,
[html_image(connector.image)],
[html_caption(connector.image)]]
rows.extend(get_additional_component_table(self, connector))
[html_caption(connector.image)],
html_table(remove_empty_columns(connector.gen_add_bom_table()), 2)]
# rows.extend(get_additional_component_table(self, connector))
# rows.append()
rows.append([html_line_breaks(connector.notes)])
html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor)))
@ -198,6 +255,15 @@ class Harness:
for cable in self.cables.values():
if isinstance(cable.bom_hash, list):
cable.bom_id = [self.bom[_hash]['bom_id'] for _hash in cable.bom_hash]
cable_bom_id_str = None
cable_bom_id_str_list = [bom_bubble(_id) for _id in cable.bom_id] if self.options.show_bom_ids else None
else:
cable.bom_id = self.bom[cable.bom_hash]['bom_id']
cable_bom_id_str = bom_bubble(cable.bom_id) if self.options.show_bom_ids else None
cable_bom_id_str_list = None
html = []
awg_fmt = ''
@ -212,7 +278,8 @@ class Harness:
rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}'
if cable.show_name else None],
[pn_info_string(HEADER_PN, None,
[cable_bom_id_str,
pn_info_string(HEADER_PN, None,
remove_links(cable.pn)) if not isinstance(cable.pn, list) else None,
html_line_breaks(pn_info_string(HEADER_MPN,
cable.manufacturer if not isinstance(cable.manufacturer, list) else None,
@ -231,7 +298,8 @@ class Harness:
[html_image(cable.image)],
[html_caption(cable.image)]]
rows.extend(get_additional_component_table(self, cable))
# rows.extend(get_additional_component_table(self, cable))
rows.append(['Reimplement additional component table!'])
rows.append([html_line_breaks(cable.notes)])
html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor)))
@ -242,6 +310,8 @@ class Harness:
for i, (connection_color, wirelabel) in enumerate(zip_longest(cable.colors, cable.wirelabels), 1):
wirehtml.append(' <tr>')
wirehtml.append(f' <td><!-- {i}_in --></td>')
if cable_bom_id_str_list:
wirehtml.append(f'<td>{cable_bom_id_str_list[i - 1]}</td>')
wirehtml.append(f' <td>')
wireinfo = []
@ -430,6 +500,16 @@ class Harness:
return data.read()
def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('pdf', )) -> None:
# bom generation
# sort BOM according to TODO key
# TODO
# assign BOM IDs
for i, (k, v) in enumerate(self.bom.items(), 1):
v['bom_id'] = i
for k, v in self.bom.items():
print(k)
print(v)
print()
# graphical output
graph = self.create_graph()
for f in fmt:
@ -437,13 +517,14 @@ class Harness:
graph.render(filename=filename, view=view, cleanup=cleanup)
graph.save(filename=f'{filename}.gv')
# bom output
bomlist = bom_list(self.bom())
with open_file_write(f'{filename}.bom.tsv') as file:
file.write(tuplelist2tsv(bomlist))
# bomlist = bom_list(self.bom())
# with open_file_write(f'{filename}.bom.tsv') as file:
# file.write(tuplelist2tsv(bomlist))
bomlist = [[]]
# HTML output
generate_html_output(filename, bomlist, self.metadata, self.options)
def bom(self):
if not self._bom:
self._bom = generate_bom(self)
return self._bom
#
# def bom(self):
# if not self._bom:
# self._bom = generate_bom(self)
# return self._bom

View File

@ -184,7 +184,7 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st
if "additional_bom_items" in yaml_data:
for line in yaml_data["additional_bom_items"]:
harness.add_bom_item(line)
harness.add_additional_bom_item(line)
if file_out is not None:
harness.output(filename=file_out, fmt=('png', 'svg'), view=False)

20
src/wireviz/wv_bom_new.py Normal file
View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from collections import namedtuple
from typing import Any, Dict, List, Optional, Tuple, Union
BOM_HASH_FIELDS = 'description unit pn manufacturer mpn supplier spn'
Bom_hash = namedtuple('Bom_hash', BOM_HASH_FIELDS)
Bom_hash_list = namedtuple('Bom_hash_list', BOM_HASH_FIELDS)
HEADER_PN = 'P/N'
HEADER_MPN = 'MPN'
HEADER_SPN = 'SPN'
def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]:
"""Return the company name and/or the part number in one single string or None otherwise."""
number = str(number).strip() if number is not None else ''
if name or number:
return f'{name if name else header}{": " + number if number else ""}'
else:
return None

View File

@ -32,6 +32,18 @@ def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_a
html.append('</table>')
return html
def html_table(rows: List[List[str]], indent_level: int = 0) -> str:
html = []
html.append('<table border="0" cellspacing="0" cellpadding="3" cellborder="1">')
for row in rows:
html.append(' <tr>')
for item in row:
html.append(f' <td>{item}</td>')
html.append(' </tr>')
html.append('</table>')
html = [' '*indent_level + row for row in html]
return '\n'.join(html)
def html_bgcolor_attr(color: Color) -> str:
"""Return attributes for bgcolor or '' if no color."""
return f' bgcolor="{translate_color(color, "HEX")}"' if color else ''
@ -74,3 +86,6 @@ def html_size_attr(image):
def html_line_breaks(inp):
return remove_links(inp).replace('\n', '<br />') if isinstance(inp, str) else inp
def bom_bubble(inp):
return(f'<table border="0"><tr><td border="1" style="rounded">{inp}</td></tr></table>')

View File

@ -116,3 +116,9 @@ def aspect_ratio(image_src):
except Exception as error:
print(f'aspect_ratio(): {type(error).__name__}: {error}')
return 1 # Assume 1:1 when unable to read actual image size
def remove_empty_columns(inp: List[List]) -> List[List]:
transp = list(map(list, zip(*inp))) # transpose list
transp = [item for item in transp if any(item)] # remove empty rows (easier)
out = list(map(list, zip(*transp))) # transpose back
return out

View File

@ -75,7 +75,7 @@ connections:
additional_bom_items:
- # define an additional item to add to the bill of materials (does not appear in graph)
description: Label, pinout information
type: Label, pinout information
qty: 2
designators:
- X2