From 8ea8248721871731ed7382106abff2f41475c1c2 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 22 Oct 2020 18:12:36 +0200 Subject: [PATCH 01/59] Squash `feature/mate+autogenerate` branch --- examples/demo02.yml | 5 +- examples/ex04.yml | 7 +- src/wireviz/DataClasses.py | 28 +++- src/wireviz/Harness.py | 52 ++++++- src/wireviz/wireviz.py | 302 +++++++++++++++++++++++-------------- src/wireviz/wv_helper.py | 18 +++ test/.gitignore | 5 + test/test1.yml | 25 +++ test/test2.yml | 25 +++ test/test3.yml | 33 ++++ test/test4.yml | 26 ++++ test/test5.yml | 64 ++++++++ test/test9.yml | 55 +++++++ tutorial/tutorial05.yml | 3 +- tutorial/tutorial06.yml | 4 +- 15 files changed, 515 insertions(+), 137 deletions(-) create mode 100644 test/.gitignore create mode 100644 test/test1.yml create mode 100644 test/test2.yml create mode 100644 test/test3.yml create mode 100644 test/test4.yml create mode 100644 test/test5.yml create mode 100644 test/test9.yml diff --git a/examples/demo02.yml b/examples/demo02.yml index 5002e7d2..d83167ff 100644 --- a/examples/demo02.yml +++ b/examples/demo02.yml @@ -22,9 +22,8 @@ connectors: X4: <<: *molex_f pinlabels: [GND, +12V, MISO, MOSI, SCK] - ferrule_crimp: + F: style: simple - autogenerate: true type: Crimp ferrule subtype: 0.25 mm² color: YE @@ -64,6 +63,6 @@ connections: - W3: [1-4] - X4: [1,3-5] - - - ferrule_crimp + - F. - W4: [1,2] - X4: [1,2] diff --git a/examples/ex04.yml b/examples/ex04.yml index 74148ecf..c3c7234e 100644 --- a/examples/ex04.yml +++ b/examples/ex04.yml @@ -8,13 +8,12 @@ cables: category: bundle connectors: - ferrule_crimp: + F: style: simple - autogenerate: true type: Crimp ferrule connections: - - - ferrule_crimp + - F. - W1: [1-6] - - ferrule_crimp + - F. diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 6b7462ab..87b7bc18 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -140,7 +140,6 @@ class Connector: 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) @@ -172,7 +171,8 @@ def __post_init__(self) -> None: raise Exception('Pins are not unique') if self.show_name is None: - self.show_name = not self.autogenerate # hide auto-generated designators by default + # hide designators for simple and for auto-generated connectors by default + self.show_name = (self.style != 'simple' and self.name[0:2] != '__') if self.show_pincount is None: self.show_pincount = self.style != 'simple' # hide pincount for simple (1 pin) connectors by default @@ -227,7 +227,7 @@ class Cable: colors: List[Colors] = field(default_factory=list) wirelabels: List[Wire] = field(default_factory=list) color_code: Optional[ColorScheme] = None - show_name: bool = True + show_name: Optional[bool] = None show_wirecount: bool = True show_wirenumbers: Optional[bool] = None ignore_in_bom: bool = False @@ -310,9 +310,11 @@ def __post_init__(self) -> None: else: raise Exception('lists of part data are only supported for bundles') - # by default, show wire numbers for cables, hide for bundles - if self.show_wirenumbers is None: - self.show_wirenumbers = self.category != 'bundle' + if self.show_name is None: + self.show_name = self.name[0:2] != '__' # hide designators for auto-generated cables by default + + if not self.show_wirenumbers: + self.show_wirenumbers = self.category != 'bundle' # by default, show wire numbers for cables, hide for bundles for i, item in enumerate(self.additional_components): if isinstance(item, dict): @@ -351,3 +353,17 @@ class Connection: via_port: Wire to_name: Optional[Designator] to_port: Optional[PinIndex] + +@dataclass +class MatePin: + from_name: Designator + from_port: PinIndex + to_name: Designator + to_port: PinIndex + shape: str + +@dataclass +class MateComponent: + from_name: Designator + to_name: Designator + shape: str diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 2f9eb641..1233be78 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -9,7 +9,7 @@ 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 Cable, Connector, MatePin, MateComponent, Metadata, Options, Tweak 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, \ @@ -19,8 +19,7 @@ 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, is_arrow @dataclass class Harness: @@ -31,6 +30,7 @@ class Harness: def __post_init__(self): self.connectors = {} self.cables = {} + self.mates = [] self._bom = [] # Internal Cache for generated bom self.additional_bom_items = [] @@ -40,6 +40,12 @@ def add_connector(self, name: str, *args, **kwargs) -> None: def add_cable(self, name: str, *args, **kwargs) -> None: self.cables[name] = Cable(name, *args, **kwargs) + def add_mate_pin(self, *args, **kwargs) -> None: + self.mates.append(MatePin(*args, **kwargs)) + + def add_mate_component(self, *args, **kwargs) -> None: + self.mates.append(MateComponent(*args, **kwargs)) + def add_bom_item(self, item: dict) -> None: self.additional_bom_items.append(item) @@ -66,7 +72,12 @@ def connect(self, from_name: str, from_pin: (int, str), via_name: str, via_wire: raise Exception(f'{name}:{pin} not found.') # check via cable - if via_name in self.cables: + if is_arrow(via_name): + if '-' in via_name: + self.mates[(from_name, from_pin, to_name, to_pin)] = via_name + elif '=' in via_name: + self.mates[(from_name, to_name)] = via_name + elif via_name in self.cables: cable = self.cables[via_name] # check if provided name is ambiguous if via_wire in cable.colors and via_wire in cable.wirelabels: @@ -114,8 +125,17 @@ def create_graph(self) -> Graph: for connection_color in cable.connections: if connection_color.from_port is not None: # connect to left self.connectors[connection_color.from_name].ports_right = True + self.connectors[connection_color.from_name].activate_pin(connection_color.from_port) if connection_color.to_port is not None: # connect to right self.connectors[connection_color.to_name].ports_left = True + self.connectors[connection_color.to_name].activate_pin(connection_color.to_port) + + for mate in self.mates: + if isinstance(mate, MatePin): + self.connectors[mate.from_name].ports_right = True + self.connectors[mate.from_name].activate_pin(mate.from_port) + self.connectors[mate.to_name].ports_left = True + self.connectors[mate.to_name].activate_pin(mate.to_port) for connector in self.connectors.values(): @@ -409,6 +429,30 @@ def typecheck(name: str, value: Any, expect: type) -> None: typecheck('tweak.append', self.tweak.append, str) dot.body.append(self.tweak.append) + for mate in self.mates: + if mate.shape[0] == '<' and mate.shape[-1] == '>': + dir = 'both' + elif mate.shape[0] == '<': + dir = 'back' + elif mate.shape[-1] == '>': + dir = 'forward' + else: + dir = 'none' + + if isinstance(mate, MatePin): + color = '#000000' + elif isinstance(mate, MateComponent): + color = '#000000:#000000' + else: + raise Exception(f'{mate} is an unknown mate') + + dot.attr('edge', color=color, style='dashed', dir=dir) + from_port = f':p{mate.from_port}r' if isinstance(mate, MatePin) and self.connectors[mate.from_name].style != 'simple' else '' + code_from = f'{mate.from_name}{from_port}:e' + to_port = f':p{mate.to_port}l' if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple' else '' + code_to = f'{mate.to_name}{to_port}:w' + dot.edge(code_from, code_to) + return dot @property diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index 95674945..e41a6356 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -15,7 +15,7 @@ from wireviz import __version__ from wireviz.DataClasses import Metadata, Options, Tweak from wireviz.Harness import Harness -from wireviz.wv_helper import expand, open_file_read +from wireviz.wv_helper import expand, get_single_key_and_value, is_arrow, open_file_read def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, str, Tuple[str]) = None) -> Any: @@ -35,20 +35,33 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st yaml_data = yaml.safe_load(yaml_input) + + # define variables ========================================================= + # containers for parsed component data and connection sets + template_connectors = {} + template_cables = {} + connection_sets = [] + # actual harness harness = Harness( metadata = Metadata(**yaml_data.get('metadata', {})), options = Options(**yaml_data.get('options', {})), tweak = Tweak(**yaml_data.get('tweak', {})), ) + # others + designators_and_templates = {} # store mapping of components to their respective template + autogenerated_designators = {} # keep track of auto-generated designators to avoid duplicates + if 'title' not in harness.metadata: harness.metadata['title'] = Path(file_out).stem # add items + # parse YAML input file ==================================================== + sections = ['connectors', 'cables', 'connections'] types = [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 sec in yaml_data and type(yaml_data[sec]) == ty: # section exists + if len(yaml_data[sec]) > 0: # section has contents if ty == dict: for key, attribs in yaml_data[sec].items(): # The Image dataclass might need to open an image file with a relative path. @@ -56,132 +69,191 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st if isinstance(image, dict): image['gv_dir'] = Path(file_out if file_out else '').parent # Inject context + # store component templates only; do not generate instances yet if sec == 'connectors': - if not attribs.get('autogenerate', False): - harness.add_connector(name=key, **attribs) + template_connectors[key] = attribs elif sec == 'cables': - harness.add_cable(name=key, **attribs) - else: - pass # section exists but is empty + template_cables[key] = attribs + else: # section exists but is empty + pass else: # section does not exist, create empty section if ty == dict: yaml_data[sec] = {} elif ty == list: yaml_data[sec] = [] - # add connections - - 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 - - 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 + connection_sets = yaml_data['connections'] + + # go through connection sets, generate and connect components ============== + + template_separator_char = '.' # TODO: make user-configurable (in case user wants to use `.` as part of their template/component names) + + def resolve_designator(inp, separator): + if separator in inp: # generate a new instance of an item + if inp.count(separator) > 1: + raise Exception(f'{inp} - Found more than one separator ({separator})') + template, designator = inp.split(separator) + if designator == '': + autogenerated_designators[template] = autogenerated_designators.get(template, 0) + 1 + designator = f'__{template}_{autogenerated_designators[template]}' + # check if redefining existing component to different template + if designator in designators_and_templates: + if designators_and_templates[designator] != template: + raise Exception(f'Trying to redefine {designator} from {designators_and_templates[designator]} to {template}') + else: + designators_and_templates[designator] = template else: - 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 - - # 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) + template, designator = (inp, inp) + if designator in designators_and_templates: + pass # referencing an exiting connector, no need to add again 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 + designators_and_templates[designator] = template + return (template, designator) + + # utilities to check for alternating connectors and cables/arrows ========== + + alternating_types = ['connector','cable/arrow'] + expected_type = None + + def check_type(designator, template, actual_type): + nonlocal expected_type + if not expected_type: # each connection set may start with either section + expected_type = actual_type + + if actual_type != expected_type: # did not alternate + raise Exception(f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}') + + def alternate_type(): # flip between connector and cable/arrow + nonlocal expected_type + expected_type = alternating_types[1 - alternating_types.index(expected_type)] + + for connection_set in connection_sets: + + # figure out number of parallel connections within this set + connectioncount = [] + for entry in connection_set: + if isinstance(entry, list): + connectioncount.append(len(entry)) + elif isinstance(entry, dict): + connectioncount.append(len(expand(list(entry.values())[0]))) # - X1: [1-4,6] yields 5 + else: + pass # strings do not reveal connectioncount + if not any(connectioncount): + raise Exception('No item in connection set revealed number of connections') + # TODO: The following should be a valid connection set, + # even though no item reveals the connection count; + # the count is not needed because only a component-level mate happens. + # - + # - CONNECTOR + # - ==> + # - CONNECTOR + + # check that all entries are the same length + if len(set(connectioncount)) > 1: + raise Exception('All items in connection set must reference the same number of connections') + # all entries are the same length, connection count is set + connectioncount = connectioncount[0] + + # expand string entries to list entries of correct length + for index, entry in enumerate(connection_set): + if isinstance(entry, str): + connection_set[index] = [entry] * connectioncount + + # resolve all designators + for index, entry in enumerate(connection_set): + if isinstance(entry, list): + for subindex, item in enumerate(entry): + template, designator = resolve_designator(item, template_separator_char) + connection_set[index][subindex] = designator + elif isinstance(entry, dict): + key = list(entry.keys())[0] + template, designator = resolve_designator(key, template_separator_char) + value = entry[key] + connection_set[index] = {designator: value} + else: + pass # string entries have been expanded in previous step + + # expand all pin lists + for index, entry in enumerate(connection_set): + if isinstance(entry, list): + connection_set[index] = [{designator: 1} for designator in entry] + elif isinstance(entry, dict): + designator = list(entry.keys())[0] + pinlist = expand(entry[designator]) + connection_set[index] = [{designator: pin} for pin in pinlist] + else: + pass # string entries have been expanded in previous step + + # Populate wiring harness ============================================== + + expected_type = None # reset check for alternating types + # at the beginning of every connection set + # since each set may begin with either type + + # generate components + for entry in connection_set: + for item in entry: + designator = list(item.keys())[0] + template = designators_and_templates[designator] + + if designator in harness.connectors: # existing connector instance + check_type(designator, template, 'connector') + elif template in template_connectors.keys(): # generate new connector instance from template + check_type(designator, template, 'connector') + harness.add_connector(name = designator, **template_connectors[template]) + + elif designator in harness.cables: # existing cable instance + check_type(designator, template, 'cable/arrow') + elif template in template_cables.keys(): # generate new cable instance from template + check_type(designator, template, 'cable/arrow') + harness.add_cable(name = designator, **template_cables[template]) + + elif is_arrow(designator): + check_type(designator, template, 'cable/arrow') + # arrows do not need to be generated here + else: + raise Exception(f'{template} is an unknown template/designator/arrow.') + + alternate_type() # entries in connection set must alternate between connectors and cables/arrows + + # transpose connection set list + # before: one item per component, one subitem per connection in set + # after: one item per connection in set, one subitem per component + connection_set = list(map(list, zip(*connection_set))) + + # connect components + for index_entry, entry in enumerate(connection_set): + for index_item, item in enumerate(entry): + designator = list(item.keys())[0] + + if designator in harness.cables: + if index_item == 0: # list started with a cable, no connector to join on left side + from_name, from_pin = (None, 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 + from_name, from_pin = get_single_key_and_value(connection_set[index_entry][index_item-1]) + via_name, via_pin = (designator, item[designator]) + if index_item == len(entry) - 1: # list ends with a cable, no connector to join on right side + to_name, to_pin = (None, None) else: - to_name = connection_list[i+1][j][0] - to_pin = connection_list[i+1][j][1] + to_name, to_pin = get_single_key_and_value(connection_set[index_entry][index_item+1]) harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) + elif is_arrow(designator): + if index_item == 0: # list starts with an arrow + raise Exception('An arrow cannot be at the start of a connection set') + elif index_item == len(entry) - 1: # list ends with an arrow + raise Exception('An arrow cannot be at the end of a connection set') + + from_name, from_pin = get_single_key_and_value(connection_set[index_entry][index_item-1]) + via_name, via_pin = (designator, None) + to_name, to_pin = get_single_key_and_value(connection_set[index_entry][index_item+1]) + if '-' in designator: # mate pin by pin + harness.add_mate_pin(from_name, from_pin, to_name, to_pin, designator) + elif '=' in designator and index_entry == 0: # mate two connectors as a whole + harness.add_mate_component(from_name, to_name, designator) + + # harness population completed ============================================= + if "additional_bom_items" in yaml_data: for line in yaml_data["additional_bom_items"]: harness.add_bom_item(line) diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 6b78f173..6b39fef4 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -65,6 +65,12 @@ def expand(yaml_data): return output +def get_single_key_and_value(d: dict): + k = list(d.keys())[0] + v = d[k] + return (k, v) + + def int2tuple(inp): if isinstance(inp, tuple): output = inp @@ -105,6 +111,18 @@ def open_file_write(filename): def open_file_append(filename): return open(filename, 'a', encoding='UTF-8') +def is_arrow(inp): + """ + Matches strings of one or multiple `-` or `=` (but not mixed) + optionally starting with `<` and/or ending with `>`. + + Examples: + <-, --, ->, <-> + <==, ==, ==>, <=> + """ + # regex by @shiraneyo + return bool(re.match(r"^\s*(?P-+|=+)(?P>?)\s*$", inp)) + def aspect_ratio(image_src): try: from PIL import Image diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..e28d9257 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,5 @@ +*.bom.tsv +*.gv +*.html +*.png +*.svg diff --git a/test/test1.yml b/test/test1.yml new file mode 100644 index 00000000..0d88a64b --- /dev/null +++ b/test/test1.yml @@ -0,0 +1,25 @@ +# based on @stmaxed's example in #134 + +connectors: + X1: &X + type: Screw connector + subtype: male + color: GN + pincount: 4 + pinlabels: [A, B, C, D] + F: + style: simple + type: Ferrule + color: GY + +cables: + W: + color: BK + colors: [BK, WH, BU, BN] + +connections: + - # ferrules + connector X1 + - W.W1: [1-4] + - F. + - --> + - X1: [1-4] diff --git a/test/test2.yml b/test/test2.yml new file mode 100644 index 00000000..a8a4c85c --- /dev/null +++ b/test/test2.yml @@ -0,0 +1,25 @@ +# based on @MSBGit's example in #134 + +connectors: + X1: &dupont + type: Dupont 2.54mm + subtype: male + pincount: 5 + color: BK + X2: + <<: *dupont + subtype: female + +cables: + W: + category: bundle + colors: [RD, BK, BU, GN] + length: 0.2 + +connections: + - + - W.W1: [1-4] + - X1: [1-4] + - ==> + - X2: [1-4] + - W.W2: [1-4] diff --git a/test/test3.yml b/test/test3.yml new file mode 100644 index 00000000..a01bedf2 --- /dev/null +++ b/test/test3.yml @@ -0,0 +1,33 @@ +# expanding upon @stmaxed's example in #134 + +connectors: + X1: &X + type: Screw connector + subtype: male + color: GN + pincount: 4 + pinlabels: [A, B, C, D] + X2: + <<: *X + subtype: female + F: + style: simple + type: Ferrule + color: GY + +cables: + W: + color: BK + colors: [BK, WH, BU, BN] + +connections: + - # ferrules + connector X1 + - W.W1: [1-4] + - F. + - --> + - X1: [1-4] + - ==> + - X2: [1-4] + - <-- + - F. + - W.W2: [1-4] diff --git a/test/test4.yml b/test/test4.yml new file mode 100644 index 00000000..85b90814 --- /dev/null +++ b/test/test4.yml @@ -0,0 +1,26 @@ +# based on @formatc1702's example in #184 + +connectors: + X: + pincount: 4 + pinlabels: [A, B, C, D] + F: + style: simple + type: ferrule + +cables: + C: + wirecount: 4 + color_code: DIN + +connections: + - + - X.X1: [1-4] + - C.C1: [1-4] + - [F.F1, F.F2, F.F3, F.F4] # generate new instances of F and assign designators + - C.C2: [1-4] + - X.X2: [1-4] + - + - [F1, F2, F3, F4] # use previously assigned designators + - C.C3: [1-4] + - X.X3: [1-4] diff --git a/test/test5.yml b/test/test5.yml new file mode 100644 index 00000000..0e670327 --- /dev/null +++ b/test/test5.yml @@ -0,0 +1,64 @@ +connectors: + XS: + type: Screw terminal connector + subtype: male + color: GN + pincount: 3 + XM: + type: Molex KK 254 + subtype: female + pincount: 3 + F: + style: simple + type: Ferrule + subtype: 0.25 mm2 + color: LB + LED_RD: &LED + type: LED + subtype: 5mm + show_pincount: false + color: RD + pins: [+, -] + pinlabels: [Anode, Cathode] + LED_GN: + <<: *LED + color: GN + LED_YE: + <<: *LED + color: YE + +cables: + C: + category: bundle + # show_name: false + colors: [RD, BK] + gauge: 0.25 mm2 + W: + category: bundle + # show_name: false + colors: [BN] + gauge: 0.25 mm2 + +connections: + - + - [F.F1, F.F2] + - C.W1: [1,2] + - LED_RD: [+,-] + - + - F.F3 + - W.W2: [1] + - LED_GN: [+] + - + - LED_GN: [-] + - W.W3: [1] + - + - XS.X1: [1-3] + - <-- + - [F1, F2, F3] + - + - LED_YE: [+,-] + - C.W4: [1,2] + - XM.X2: [1,2] + - + - W3: [1] + - X2: [3] diff --git a/test/test9.yml b/test/test9.yml new file mode 100644 index 00000000..bad0256f --- /dev/null +++ b/test/test9.yml @@ -0,0 +1,55 @@ +connectors: + JSTMALE: &JST_SM # use generic names here, assign designators at generation time + type: JST SM + subtype: male + pincount: 4 + pinlabels: [A, B, C, D] + JSTFEMALE: + <<: *JST_SM # easily create JSTMALE's matching connector + subtype: female + X4: # this connector is only used once, use fixed designator here already + type: Screw terminal connector + pincount: 4 + color: GN + pinlabels: [W, X, Y, Z] + S: + style: simple + type: Splice + color: CU + F: + style: simple + type: Ferrule + color: GY + + +cables: + CABLE: + wirecount: 4 + color_code: DIN + length: 0.1 + WIRE: + wirecount: 1 + colors: [BK] + length: 0.1 + +connections: + - + - JSTMALE.X1: [4-1] # use `.` syntax to generate a new instance of JSTMALE, named X1 + - CABLE.W1: [1-4] # same syntax for cables + - [S., S., S.S1, S.] # splice W1 and W2 together; only wire #3 needs a user-defined designator + - CABLE.W2: [1-4] + - S. # test shorthand, auto-get required number of ferrules from context + - CABLE.W21: [1-4] + - JSTFEMALE.X2: [1-4] + - <=> # mate X2 and X3 + - JSTMALE.X3: [1-4] + - CABLE.W3: [1-4] + - [F., F., F., F.] + - --> # insert ferrules into screw terminal connector + - X4: [2,1,4,3] # X4 does not require auto-generation, thus no `.` syntax here + - + - S1: [1] # reuse previously generated splice + # TODO: Make it work with `- F1` only, making pin 1 is implied + - WIRE.: [1] # We don't care about a simple wire's designator, auto-generate please! + # TODO: Make it work with `- W.W4: 1`, dropping the need for `[]` + - X2: [4] diff --git a/tutorial/tutorial05.yml b/tutorial/tutorial05.yml index e894a452..d4739c55 100644 --- a/tutorial/tutorial05.yml +++ b/tutorial/tutorial05.yml @@ -5,7 +5,6 @@ connectors: subtype: female F1: style: simple - autogenerate: true type: Crimp ferrule subtype: 0.5 mm² color: OG # optional color @@ -19,6 +18,6 @@ cables: connections: - - - F1 # a new ferrule is auto-generated for each of the four wires + - F1. # a new ferrule is auto-generated for each of the four wires - W1: [1-4] - X1: [1-4] diff --git a/tutorial/tutorial06.yml b/tutorial/tutorial06.yml index 51431573..0cd79b69 100644 --- a/tutorial/tutorial06.yml +++ b/tutorial/tutorial06.yml @@ -5,13 +5,11 @@ connectors: subtype: female F_10: # this is a unique ferrule style: simple - show_name: false # non-autogenerated connectors show their name by default; override type: Crimp ferrule subtype: 1.0 mm² color: YE # optional color F_05: # this is a ferrule that will be auto-generated on demand style: simple - autogenerate: true type: Crimp ferrule subtype: 0.5 mm² color: OG @@ -25,6 +23,6 @@ cables: connections: - - - [F_05, F_10, F_10, F_05] + - [F_05., F_10.F1, F_10.F1, F_05.] - W1: [1-4] - X1: [1-4] From 2a62dae9eec5fce017ce17335b154d6632753856 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 23 Sep 2021 16:20:59 +0200 Subject: [PATCH 02/59] Resolve edge case of empty HTML tables --- src/wireviz/wv_gv_html.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wireviz/wv_gv_html.py b/src/wireviz/wv_gv_html.py index 0b843db5..ff8b4eaa 100644 --- a/src/wireviz/wv_gv_html.py +++ b/src/wireviz/wv_gv_html.py @@ -14,6 +14,8 @@ def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_a # attributes in any leading inside a list are injected into to the preceeding tag html = [] html.append(f'') + + num_rows = 0 for row in rows: if isinstance(row, List): if len(row) > 0 and any(row): @@ -25,10 +27,14 @@ def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_a html.append(f' '.replace('>
{cell}
') html.append(' ') + num_rows = num_rows + 1 elif row is not None: html.append(' ') html.append(f' {row}') html.append(' ') + num_rows = num_rows + 1 + if num_rows == 0: # empty table + html.append('') # generate empty cell to avoid GraphViz errors html.append('') return html From 2d701ee652827990ca64088c9808138c636a1e9d Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 23 Sep 2021 16:52:21 +0200 Subject: [PATCH 03/59] Resolve component level mate not revealing connection count --- src/wireviz/wireviz.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index e41a6356..7818104e 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -139,10 +139,12 @@ def alternate_type(): # flip between connector and cable/arrow else: pass # strings do not reveal connectioncount if not any(connectioncount): - raise Exception('No item in connection set revealed number of connections') - # TODO: The following should be a valid connection set, - # even though no item reveals the connection count; - # the count is not needed because only a component-level mate happens. + # no item in the list revealed connection count; + # assume connection count is 1 + connectioncount = [1] + # Example: The following is a valid connection set, + # even though no item reveals the connection count; + # the count is not needed because only a component-level mate happens. # - # - CONNECTOR # - ==> From 9ccd55ef9327f4fd19ede10b8da9b9ba3f288270 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 23 Sep 2021 17:02:17 +0200 Subject: [PATCH 04/59] Update syntax description (autogeneration, arrows) Moved metadata and options info further down, so that the core functionality (connectors, cables, connection sets) comes first. --- docs/syntax.md | 152 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 14 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index 8da04aee..2859fdb7 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -85,22 +85,8 @@ tweak: # optional tweaking of .gv output # loops loops: # every list item is itself a list of exactly two pins # on the connector that are to be shorted - - # auto-generation - autogenerate: # optional; defaults to false; see below - ``` -### Auto-generation of connectors - -The `autogenerate: true` option is especially useful for very simple, recurring connectors such as crimp ferrules, splices, and others, where it would be a hassle to individually assign unique designators for every instance. - -By default, when defining a connector, it will be generated once using the specified designator, and can be referenced multiple times, in different connection sets (see below). - -If `autogenerate: true` is set, the connector will _not_ be generated at first. When defining the `connections` section (see below), every time the connector is mentioned, a new instance with an auto-incremented designator is generated and attached. - -Since the auto-incremented and auto-assigned designator is not known to the user, one instance of the connector can not be referenced again outside the point of creation. The `autogenerate: true` option is therefore only useful for terminals with only one wire attached, or splices with exactly one wire going in, and one wire going out. If more wires are to be attached (e.g. for a three-way splice, or a crimp where multiple wires are joined), a separate connector with `autogenerate: false` and a user-defined, unique designator needs to be used. - ## Cable attributes ```yaml @@ -173,6 +159,7 @@ connections: - # Each list entry is a connection set - # Each connection set is itself a list of items - # Items must alternatingly belong to the connectors and cables sections + # Arrows may be used instead of cables -... - # example (single connection) @@ -189,6 +176,18 @@ connections: - [, ..., ] # specify multiple simple connectors to attach in parallel # these may be unique, auto-generated, or a mix of both + - # example (arrows between pins) + - : [, ..., ] + - [, ..., ] # draw arrow linking pins of both connectors + # use single line arrows (--, <--, <-->, -->) + - : [, ..., ] + + - # example (arrows between connectors) + - + - # draw arrow linking the connectors themselves + # use double line arrow (==, <==, <==>, ==>) + - + ... ``` @@ -199,6 +198,7 @@ connections: - When a connection set defines multiple parallel connections, the number of specified ``s and ``s for each component in the set must match. When specifying only one designator, one is auto-generated for each connection of the set. - `` may reference a pin's unique ID (as per the connector's `pins` attribute, auto-numbered from 1 by default) or its label (as per `pinlabels`). - `` may reference a wire's number within a cable/bundle, its label (as per `wirelabels`) or, if unambiguous, its color. +- For ``, see below. ### Single connections @@ -249,6 +249,130 @@ For connectors with `autogenerate: true`, a new instance, with auto-generated de - `-` auto-expands to a range. - `` to refer to a wire's label or color, if unambiguous. +### Arrows + +Arrows may be used in place of wires to join two connectors. This can represent the mating of matching connectors. + +To represent joining individual pins between two connectors, a list of single arrows is used: +```yaml +connections: + - + - : [,...,] + - [, ..., ] # --, <--, <--> or --> + - : [,...,] +``` + +To represent mating of two connectors as a whole, one double arrow is used: +```yaml +connections: + - + - # using connector designator only + - # ==, <==, <==> or ==> + - + - + - ... + - : [, ...] # designator and pinlist (pinlist is ignored) + # useful when combining arrows and wires + - # ==, <==, <==> or ==> + - : [, ...] + - ... +``` + +### Autogeneration of items + +For very simple, recurring connectors such as crimp ferrules, splices and others, where it would be a hassle to individually assign unique designators for every instance, autogeneration may be used. Both connectors and cables can be autogenerated. + +Example (see `connections` section): + +```yaml +connectors: + X: + # ... + Y: + # ... + Z: + style: simple + # ... +cables: + V: + # ... + W: + # ... + +connections: + - # no autogeneration (normal use) + - X: [1,2,...] # Use X as both the template and the instance designator + - V: [1,2,...] # Use V as both the template and the instance designator + # ... + + - # autogeneration of named instances + - Y.Y1: [1,2,...] # Use template Y, generate instance with designator Y1 + - W.W1: [1,2,...] # Use template W, generate instance with designator W1 + - Y.Y2: [1,2,...] # generate more instances from the same templates + - W.W2: [1,2,...] + - Y.Y3: [1,2,...] + + - # autogeneration of unnamed instances + - Y3: [1,2,...] # reuse existing instance Y3 + - W.W4: [1,2,...] + - Z. # Use template Z, generate one unnamed instance + # for each connection in set +``` + +Since the internally assigned designator of an unnamed component is not known to the user, one instance of the connector can not be referenced again outside the point of creation (i.e. in other connection sets, or later in the same set). Autogeneration of unnamed instances is therefore only useful for terminals with only one wire attached, or splices with exactly one wire going in, and one wire going out. +If a component is to be used in other connection sets (e.g. for a three-way splice, or a crimp where multiple wires are joined), a named instance needs to be used. + +Names of autogenerated components are hidden by default. While they can be shown in the graphical output using the `show_name: true` option, it is not recommended to manually use the internally assigned designator (starting with a double underscore `__`), since it might change in future WireViz versions, or when the order of items in connection sets changes. + + +## Metadata entries + +```yaml + # Meta-information describing the harness + + # Each key/value pair replaces all key references in + # the HTML output template with the belonging value. + # Typical keys are 'title', 'description', and 'notes', + # but any key is accepted. Unused keys are ignored. + : # Any valid YAML syntax is accepted + # If no value is specified for 'title', then the + # output filename without extension is used. +``` + +## Options + +```yaml + # Common attributes for the whole harness. + # All entries are optional and have default values. + + # Background color of diagram and HTML output + bgcolor: # Default = 'WH' + + # Background color of other diagram elements + bgcolor_node: # Default = 'WH' + bgcolor_connector: # Default = bgcolor_node + bgcolor_cable: # Default = bgcolor_node + bgcolor_bundle: # Default = bgcolor_cable + + # How to display colors as text in the diagram + # 'full' : Lowercase full color name + # 'FULL' : Uppercase full color name + # 'hex' : Lowercase hexadecimal values + # 'HEX' : Uppercase hexadecimal values + # 'short': Lowercase short color name + # 'SHORT': Uppercase short color name + # 'ger' : Lowercase short German color name + # 'GER' : Uppercase short German color name + color_mode: # Default = 'SHORT' + + # Fontname to use in diagram and HTML output + fontname: # Default = 'arial' + + # If True, show only a BOM entry reference together with basic info + # about additional components inside the diagram node (connector/cable box). + # If False, show all info about additional components inside the diagram node. + mini_bom_mode: # Default = True +``` ## Metadata entries From db6f2da232ba953184326f24993fbf96628ccbbf Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 23 Sep 2021 17:07:06 +0200 Subject: [PATCH 05/59] Move selected test files to examples directory --- test/test1.yml => examples/ex11.yml | 0 test/test2.yml => examples/ex12.yml | 0 test/test4.yml => examples/ex13.yml | 0 test/test9.yml => examples/ex14.yml | 0 test/.gitignore | 5 --- test/test3.yml | 33 --------------- test/test5.yml | 64 ----------------------------- 7 files changed, 102 deletions(-) rename test/test1.yml => examples/ex11.yml (100%) rename test/test2.yml => examples/ex12.yml (100%) rename test/test4.yml => examples/ex13.yml (100%) rename test/test9.yml => examples/ex14.yml (100%) delete mode 100644 test/.gitignore delete mode 100644 test/test3.yml delete mode 100644 test/test5.yml diff --git a/test/test1.yml b/examples/ex11.yml similarity index 100% rename from test/test1.yml rename to examples/ex11.yml diff --git a/test/test2.yml b/examples/ex12.yml similarity index 100% rename from test/test2.yml rename to examples/ex12.yml diff --git a/test/test4.yml b/examples/ex13.yml similarity index 100% rename from test/test4.yml rename to examples/ex13.yml diff --git a/test/test9.yml b/examples/ex14.yml similarity index 100% rename from test/test9.yml rename to examples/ex14.yml diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index e28d9257..00000000 --- a/test/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*.bom.tsv -*.gv -*.html -*.png -*.svg diff --git a/test/test3.yml b/test/test3.yml deleted file mode 100644 index a01bedf2..00000000 --- a/test/test3.yml +++ /dev/null @@ -1,33 +0,0 @@ -# expanding upon @stmaxed's example in #134 - -connectors: - X1: &X - type: Screw connector - subtype: male - color: GN - pincount: 4 - pinlabels: [A, B, C, D] - X2: - <<: *X - subtype: female - F: - style: simple - type: Ferrule - color: GY - -cables: - W: - color: BK - colors: [BK, WH, BU, BN] - -connections: - - # ferrules + connector X1 - - W.W1: [1-4] - - F. - - --> - - X1: [1-4] - - ==> - - X2: [1-4] - - <-- - - F. - - W.W2: [1-4] diff --git a/test/test5.yml b/test/test5.yml deleted file mode 100644 index 0e670327..00000000 --- a/test/test5.yml +++ /dev/null @@ -1,64 +0,0 @@ -connectors: - XS: - type: Screw terminal connector - subtype: male - color: GN - pincount: 3 - XM: - type: Molex KK 254 - subtype: female - pincount: 3 - F: - style: simple - type: Ferrule - subtype: 0.25 mm2 - color: LB - LED_RD: &LED - type: LED - subtype: 5mm - show_pincount: false - color: RD - pins: [+, -] - pinlabels: [Anode, Cathode] - LED_GN: - <<: *LED - color: GN - LED_YE: - <<: *LED - color: YE - -cables: - C: - category: bundle - # show_name: false - colors: [RD, BK] - gauge: 0.25 mm2 - W: - category: bundle - # show_name: false - colors: [BN] - gauge: 0.25 mm2 - -connections: - - - - [F.F1, F.F2] - - C.W1: [1,2] - - LED_RD: [+,-] - - - - F.F3 - - W.W2: [1] - - LED_GN: [+] - - - - LED_GN: [-] - - W.W3: [1] - - - - XS.X1: [1-3] - - <-- - - [F1, F2, F3] - - - - LED_YE: [+,-] - - C.W4: [1,2] - - XM.X2: [1,2] - - - - W3: [1] - - X2: [3] From 50ea7f5771c40168f9b1de651f9e003835b29619 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Wed, 13 Oct 2021 21:45:22 +0200 Subject: [PATCH 06/59] Fix duplicates in `syntax.md` after rebase --- docs/syntax.md | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index 2859fdb7..1c14d3e8 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -375,55 +375,6 @@ Names of autogenerated components are hidden by default. While they can be shown ``` -## Metadata entries - -```yaml - # Meta-information describing the harness - - # Each key/value pair replaces all key references in - # the HTML output template with the belonging value. - # Typical keys are 'title', 'description', and 'notes', - # but any key is accepted. Unused keys are ignored. - : # Any valid YAML syntax is accepted - # If no value is specified for 'title', then the - # output filename without extension is used. -``` - -## Options - -```yaml - # Common attributes for the whole harness. - # All entries are optional and have default values. - - # Background color of diagram and HTML output - bgcolor: # Default = 'WH' - - # Background color of other diagram elements - bgcolor_node: # Default = 'WH' - bgcolor_connector: # Default = bgcolor_node - bgcolor_cable: # Default = bgcolor_node - bgcolor_bundle: # Default = bgcolor_cable - - # How to display colors as text in the diagram - # 'full' : Lowercase full color name - # 'FULL' : Uppercase full color name - # 'hex' : Lowercase hexadecimal values - # 'HEX' : Uppercase hexadecimal values - # 'short': Lowercase short color name - # 'SHORT': Uppercase short color name - # 'ger' : Lowercase short German color name - # 'GER' : Uppercase short German color name - color_mode: # Default = 'SHORT' - - # Fontname to use in diagram and HTML output - fontname: # Default = 'arial' - - # If True, show only a BOM entry reference together with basic info - # about additional components inside the diagram node (connector/cable box). - # If False, show all info about additional components inside the diagram node. - mini_bom_mode: # Default = True -``` - ## BOM items and additional components Connectors (both regular, and auto-generated), cables, and wires of a bundle are automatically added to the BOM, From 02a800abef3f35bdc4f8bbcafd9f50309a7cbe17 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 14 Oct 2021 18:03:18 +0200 Subject: [PATCH 07/59] Fix bug of arrows using the wrong port IDs --- src/wireviz/Harness.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 1233be78..1b01fa0f 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -40,11 +40,13 @@ def add_connector(self, name: str, *args, **kwargs) -> None: def add_cable(self, name: str, *args, **kwargs) -> None: self.cables[name] = Cable(name, *args, **kwargs) - def add_mate_pin(self, *args, **kwargs) -> None: - self.mates.append(MatePin(*args, **kwargs)) + def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_type) -> None: + from_pin_id = self.connectors[from_name].pins.index(from_pin) + to_pin_id = self.connectors[to_name].pins.index(to_pin) + self.mates.append(MatePin(from_name, from_pin_id, to_name, to_pin_id, arrow_type)) - def add_mate_component(self, *args, **kwargs) -> None: - self.mates.append(MateComponent(*args, **kwargs)) + def add_mate_component(self, from_name, to_name, arrow_type) -> None: + self.mates.append(MateComponent(from_name, to_name, arrow_type)) def add_bom_item(self, item: dict) -> None: self.additional_bom_items.append(item) @@ -447,9 +449,9 @@ def typecheck(name: str, value: Any, expect: type) -> None: raise Exception(f'{mate} is an unknown mate') dot.attr('edge', color=color, style='dashed', dir=dir) - from_port = f':p{mate.from_port}r' if isinstance(mate, MatePin) and self.connectors[mate.from_name].style != 'simple' else '' + from_port = f':p{mate.from_port+1}r' if isinstance(mate, MatePin) and self.connectors[mate.from_name].style != 'simple' else '' code_from = f'{mate.from_name}{from_port}:e' - to_port = f':p{mate.to_port}l' if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple' else '' + to_port = f':p{mate.to_port+1}l' if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple' else '' code_to = f'{mate.to_name}{to_port}:w' dot.edge(code_from, code_to) From f0b63de3c7e082b5bad1eefecc5bee6feabe6ca1 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 14 Oct 2021 18:03:30 +0200 Subject: [PATCH 08/59] Simplify code --- src/wireviz/wireviz.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index 7818104e..abf7a20a 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -232,12 +232,12 @@ def alternate_type(): # flip between connector and cable/arrow if index_item == 0: # list started with a cable, no connector to join on left side from_name, from_pin = (None, None) else: - from_name, from_pin = get_single_key_and_value(connection_set[index_entry][index_item-1]) + from_name, from_pin = get_single_key_and_value(entry[index_item-1]) via_name, via_pin = (designator, item[designator]) if index_item == len(entry) - 1: # list ends with a cable, no connector to join on right side to_name, to_pin = (None, None) else: - to_name, to_pin = get_single_key_and_value(connection_set[index_entry][index_item+1]) + to_name, to_pin = get_single_key_and_value(entry[index_item+1]) harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) elif is_arrow(designator): @@ -246,9 +246,11 @@ def alternate_type(): # flip between connector and cable/arrow elif index_item == len(entry) - 1: # list ends with an arrow raise Exception('An arrow cannot be at the end of a connection set') - from_name, from_pin = get_single_key_and_value(connection_set[index_entry][index_item-1]) +# self.connectors[from_name].pins.index(from_pin) + # import pudb; pu.db + from_name, from_pin = get_single_key_and_value(entry[index_item-1]) via_name, via_pin = (designator, None) - to_name, to_pin = get_single_key_and_value(connection_set[index_entry][index_item+1]) + to_name, to_pin = get_single_key_and_value(entry[index_item+1]) if '-' in designator: # mate pin by pin harness.add_mate_pin(from_name, from_pin, to_name, to_pin, designator) elif '=' in designator and index_entry == 0: # mate two connectors as a whole From 6b1e274d57ffc014fc0081c6679e19586da7ba2b Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Sat, 16 Oct 2021 21:46:31 +0200 Subject: [PATCH 09/59] Refactor functions for harness building - Use pin names instead of pin indices, until the last moment when generating the ports for the GraphViz nodes - `Harness.add_mate_pin()` now uses pin names - Remove unused `if is_arrow()` check from `Harness.connect()` - Consolidate calling of `Connector.activate_pin()` to prevent subtle bugs - Call it from `connect()` and `add_mate_pin()` - No longer call it from `create_graph()` - Misc. other tuning --- src/wireviz/DataClasses.py | 21 +++++++--- src/wireviz/Harness.py | 83 +++++++++++++++++--------------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 87b7bc18..6e87bb6a 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from enum import Enum, auto from typing import Dict, List, Optional, Tuple, Union from dataclasses import dataclass, field, InitVar from pathlib import Path @@ -23,11 +24,17 @@ Pin = Union[int, PlainText] # Pin identifier PinIndex = int # Zero-based pin index Wire = Union[int, PlainText] # Wire number or Literal['s'] for shield +NoneOrMorePins = Union[Pin, Tuple[Pin, ...], None] # None, one, or a tuple of pin identifiers NoneOrMorePinIndices = Union[PinIndex, Tuple[PinIndex, ...], None] # None, one, or a tuple of zero-based pin indices OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires # Metadata can contain whatever is needed by the HTML generation/template. MetadataKeys = PlainText # Literal['title', 'description', 'notes', ...] + +class Side(Enum): + LEFT = auto() + RIGHT = auto() + class Metadata(dict): pass @@ -188,8 +195,12 @@ def __post_init__(self) -> None: if isinstance(item, dict): self.additional_components[i] = AdditionalComponent(**item) - def activate_pin(self, pin: Pin) -> None: + def activate_pin(self, pin: Pin, side: Side) -> None: self.visible_pins[pin] = True + if side == Side.LEFT: + self.ports_left = True + elif side == Side.RIGHT: + self.ports_right = True def get_qty_multiplier(self, qty_multiplier: Optional[ConnectorMultiplier]) -> int: if not qty_multiplier: @@ -349,17 +360,17 @@ def get_qty_multiplier(self, qty_multiplier: Optional[CableMultiplier]) -> float @dataclass class Connection: from_name: Optional[Designator] - from_port: Optional[PinIndex] + from_pin: Optional[Pin] via_port: Wire to_name: Optional[Designator] - to_port: Optional[PinIndex] + to_pin: Optional[Pin] @dataclass class MatePin: from_name: Designator - from_port: PinIndex + from_pin: Pin to_name: Designator - to_port: PinIndex + to_pin: Pin shape: str @dataclass diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 1b01fa0f..5ef25307 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -9,7 +9,7 @@ import re from wireviz import wv_colors, __version__, APP_NAME, APP_URL -from wireviz.DataClasses import Cable, Connector, MatePin, MateComponent, Metadata, Options, Tweak +from wireviz.DataClasses import Cable, Connector, MatePin, MateComponent, Metadata, Options, Tweak, Side 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, \ @@ -41,9 +41,9 @@ def add_cable(self, name: str, *args, **kwargs) -> None: self.cables[name] = Cable(name, *args, **kwargs) def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_type) -> None: - from_pin_id = self.connectors[from_name].pins.index(from_pin) - to_pin_id = self.connectors[to_name].pins.index(to_pin) - self.mates.append(MatePin(from_name, from_pin_id, to_name, to_pin_id, arrow_type)) + self.mates.append(MatePin(from_name, from_pin, to_name, to_pin, arrow_type)) + self.connectors[from_name].activate_pin(from_pin, Side.RIGHT) + self.connectors[to_name].activate_pin(to_pin, Side.LEFT) def add_mate_component(self, from_name, to_name, arrow_type) -> None: self.mates.append(MateComponent(from_name, to_name, arrow_type)) @@ -74,12 +74,7 @@ def connect(self, from_name: str, from_pin: (int, str), via_name: str, via_wire: raise Exception(f'{name}:{pin} not found.') # check via cable - if is_arrow(via_name): - if '-' in via_name: - self.mates[(from_name, from_pin, to_name, to_pin)] = via_name - elif '=' in via_name: - self.mates[(from_name, to_name)] = via_name - elif via_name in self.cables: + if via_name in self.cables: cable = self.cables[via_name] # check if provided name is ambiguous if via_wire in cable.colors and via_wire in cable.wirelabels: @@ -95,14 +90,12 @@ def connect(self, from_name: str, from_pin: (int, str), via_name: str, via_wire: raise Exception(f'{via_name}:{via_wire} is used for more than one wire.') via_wire = cable.wirelabels.index(via_wire) + 1 # list index starts at 0, wire IDs start at 1 - from_pin_id = self.connectors[from_name].pins.index(from_pin) if from_pin is not None else None - to_pin_id = self.connectors[to_name].pins.index(to_pin) if to_pin is not None else None - - self.cables[via_name].connect(from_name, from_pin_id, via_wire, to_name, to_pin_id) + # perform the actual connection + self.cables[via_name].connect(from_name, from_pin, via_wire, to_name, to_pin) if from_name in self.connectors: - self.connectors[from_name].activate_pin(from_pin) + self.connectors[from_name].activate_pin(from_pin, Side.RIGHT) if to_name in self.connectors: - self.connectors[to_name].activate_pin(to_pin) + self.connectors[to_name].activate_pin(to_pin, Side.LEFT) def create_graph(self) -> Graph: dot = Graph() @@ -122,23 +115,6 @@ def create_graph(self) -> Graph: dot.attr('edge', style='bold', fontname=self.options.fontname) - # prepare ports on connectors depending on which side they will connect - for _, cable in self.cables.items(): - for connection_color in cable.connections: - if connection_color.from_port is not None: # connect to left - self.connectors[connection_color.from_name].ports_right = True - self.connectors[connection_color.from_name].activate_pin(connection_color.from_port) - if connection_color.to_port is not None: # connect to right - self.connectors[connection_color.to_name].ports_left = True - self.connectors[connection_color.to_name].activate_pin(connection_color.to_port) - - for mate in self.mates: - if isinstance(mate, MatePin): - self.connectors[mate.from_name].ports_right = True - self.connectors[mate.from_name].activate_pin(mate.from_port) - self.connectors[mate.to_name].ports_left = True - self.connectors[mate.to_name].activate_pin(mate.to_port) - for connector in self.connectors.values(): # If no wires connected (except maybe loop wires)? @@ -341,32 +317,34 @@ def create_graph(self) -> Graph: else: # it's a shield connection # shield is shown with specified color and black borders, or as a thin black wire otherwise dot.attr('edge', color=':'.join(['#000000', shield_color_hex, '#000000']) if isinstance(cable.shield, str) else '#000000') - if connection.from_port is not None: # connect to left + if connection.from_pin is not None: # connect to left from_connector = self.connectors[connection.from_name] - from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else '' - code_left_1 = f'{connection.from_name}{from_port}:e' + from_pin_index = from_connector.pins.index(connection.from_pin) + from_port_str = f':p{from_pin_index+1}r' if from_connector.style != 'simple' else '' + code_left_1 = f'{connection.from_name}{from_port_str}:e' code_left_2 = f'{cable.name}:w{connection.via_port}:w' dot.edge(code_left_1, code_left_2) if from_connector.show_name: - from_info = [str(connection.from_name), str(self.connectors[connection.from_name].pins[connection.from_port])] + from_info = [str(connection.from_name), str(connection.from_pin)] if from_connector.pinlabels: - pinlabel = from_connector.pinlabels[connection.from_port] + pinlabel = from_connector.pinlabels[from_pin_index] if pinlabel != '': from_info.append(pinlabel) from_string = ':'.join(from_info) else: from_string = '' html = [row.replace(f'', from_string) for row in html] - if connection.to_port is not None: # connect to right + if connection.to_pin is not None: # connect to right to_connector = self.connectors[connection.to_name] + to_pin_index = to_connector.pins.index(connection.to_pin) + to_port_str = f':p{to_pin_index+1}l' if to_connector.style != 'simple' else '' code_right_1 = f'{cable.name}:w{connection.via_port}:e' - to_port = f':p{connection.to_port+1}l' if self.connectors[connection.to_name].style != 'simple' else '' - code_right_2 = f'{connection.to_name}{to_port}:w' + code_right_2 = f'{connection.to_name}{to_port_str}:w' dot.edge(code_right_1, code_right_2) if to_connector.show_name: - to_info = [str(connection.to_name), str(self.connectors[connection.to_name].pins[connection.to_port])] + to_info = [str(connection.to_name), str(connection.to_pin)] if to_connector.pinlabels: - pinlabel = to_connector.pinlabels[connection.to_port] + pinlabel = to_connector.pinlabels[to_pin_index] if pinlabel != '': to_info.append(pinlabel) to_string = ':'.join(to_info) @@ -448,11 +426,22 @@ def typecheck(name: str, value: Any, expect: type) -> None: else: raise Exception(f'{mate} is an unknown mate') + from_connector = self.connectors[mate.from_name] + if isinstance(mate, MatePin) and self.connectors[mate.from_name].style != 'simple': + from_pin_index = from_connector.pins.index(mate.from_pin) + from_port_str = f':p{from_pin_index+1}r' + else: # MateComponent or style == 'simple' + from_port_str = '' + if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple': + to_pin_index = to_connector.pins.index(mate.to_pin) + to_port_str = f':p{to_pin_index+1}l' if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple' else '' + else: # MateComponent or style == 'simple' + to_port_str = '' + code_from = f'{mate.from_name}{from_port_str}:e' + to_connector = self.connectors[mate.to_name] + code_to = f'{mate.to_name}{to_port_str}:w' + dot.attr('edge', color=color, style='dashed', dir=dir) - from_port = f':p{mate.from_port+1}r' if isinstance(mate, MatePin) and self.connectors[mate.from_name].style != 'simple' else '' - code_from = f'{mate.from_name}{from_port}:e' - to_port = f':p{mate.to_port+1}l' if isinstance(mate, MatePin) and self.connectors[mate.to_name].style != 'simple' else '' - code_to = f'{mate.to_name}{to_port}:w' dot.edge(code_from, code_to) return dot From eae2694b5d512e088395d119ba87c038282c635e Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 26 Aug 2021 18:50:08 +0200 Subject: [PATCH 10/59] Implement template-based HTML output --- src/wireviz/templates/din-6771.html | 284 ++++++++++++++++++++++++++++ src/wireviz/templates/simple.html | 45 +++++ src/wireviz/wv_helper.py | 19 ++ src/wireviz/wv_html.py | 120 +++++++----- 4 files changed, 424 insertions(+), 44 deletions(-) create mode 100644 src/wireviz/templates/din-6771.html create mode 100644 src/wireviz/templates/simple.html diff --git a/src/wireviz/templates/din-6771.html b/src/wireviz/templates/din-6771.html new file mode 100644 index 00000000..aec0f901 --- /dev/null +++ b/src/wireviz/templates/din-6771.html @@ -0,0 +1,284 @@ + + + + + + + <!-- %title% --> + + + + +
+
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateName
Sheet
of
RevChangelogDateName
+
+ +
+
+ + diff --git a/src/wireviz/templates/simple.html b/src/wireviz/templates/simple.html new file mode 100644 index 00000000..961ef9db --- /dev/null +++ b/src/wireviz/templates/simple.html @@ -0,0 +1,45 @@ + + + + + <!-- %title% --> + + +

+

Diagram

+ +
+ +
+ +
+ +
+ +
+ +
+ +

Bill of Materials

+ +
+ +
+ + diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 6b39fef4..d5d6456d 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from typing import List +from pathlib import Path import re awg_equiv_table = { @@ -134,3 +135,21 @@ 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 smart_file_resolve(filename, possible_paths): + filename = Path(filename) + if filename.is_absolute(): + if filename.exists(): + return filename + else: + raise Exception(f'{filename} does not exist.') + else: # search all possible paths in decreasing order of precedence + possible_paths = [Path(path).resolve() for path in possible_paths] + for possible_path in possible_paths: + resolved_path = (possible_path / filename).resolve() + if (resolved_path).exists(): + return resolved_path + else: + raise Exception(f'{filename} was not found in any of the following locations: \n' + + '\n'.join([str(x) for x in possible_paths])) diff --git a/src/wireviz/wv_html.py b/src/wireviz/wv_html.py index 0b819746..09bba26f 100644 --- a/src/wireviz/wv_html.py +++ b/src/wireviz/wv_html.py @@ -1,54 +1,86 @@ # -*- coding: utf-8 -*- from pathlib import Path -from typing import List, Union +from typing import Dict, List, Union import re from wireviz import __version__, APP_NAME, APP_URL, wv_colors from wireviz.DataClasses import Metadata, Options -from wireviz.wv_helper import flatten2d, open_file_read, open_file_write +from wireviz.wv_helper import flatten2d, open_file_read, open_file_write, smart_file_resolve +from wireviz.wv_gv_html import html_line_breaks def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], metadata: Metadata, options: Options): + + # load HTML template + if 'name' in metadata.get('template',{}): + # if relative path to template was provided, check directory of YAML file first, fall back to built-in template directory + templatefile = smart_file_resolve(f'{metadata["template"]["name"]}.html', [Path(filename).parent, Path(__file__).parent / 'templates']) + else: + # fall back to built-in simple template if no template was provided + templatefile = Path(__file__).parent / 'templates/simple.html' + + with open_file_read(templatefile) as file: + html = file.read() + + # embed SVG diagram + with open_file_read(f'{filename}.svg') as file: + svgdata = file.read() + svgdata = re.sub( + '^<[?]xml [^?>]*[?]>[^<]*]*>', + '', + svgdata, 1) + html = html.replace('', svgdata) + + # generate BOM table + bom = flatten2d(bom_list) + + # generate BOM header (may be at the top or bottom of the table) + bom_header_html = ' \n' + for item in bom[0]: + th_class = f'bom_col_{item.lower()}' + bom_header_html = f'{bom_header_html} {item}\n' + bom_header_html = f'{bom_header_html} \n' + + # generate BOM contents + bom_contents = [] + for row in bom[1:]: + row_html = ' \n' + for i, item in enumerate(row): + td_class = f'bom_col_{bom[0][i].lower()}' + row_html = f'{row_html} {item}\n' + row_html = f'{row_html} \n' + bom_contents.append(row_html) + + bom_html = '\n' + bom_header_html + ''.join(bom_contents) + '
\n' + bom_html_reversed = '\n' + ''.join(list(reversed(bom_contents))) + bom_header_html + '
\n' + + # insert BOM table + html = html.replace('', bom_html) + html = html.replace('', bom_html_reversed) + + # insert generator + html = html.replace('', f'{APP_NAME} {__version__} - {APP_URL}') + + # insert other metadata + if metadata: + + html = html.replace(f'"sheetsize_default"', '"{}"'.format(metadata.get('template',{}).get('sheetsize', ''))) # include quotes so no replacement happens within - +

Diagram

From 5bed6de7ab6e8e1e2227362bf4adb247a941c312 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Tue, 28 Sep 2021 20:49:18 +0200 Subject: [PATCH 14/59] Consolidate code for replacing HTML placeholders --- src/wireviz/wv_html.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/wireviz/wv_html.py b/src/wireviz/wv_html.py index 09bba26f..73881f87 100644 --- a/src/wireviz/wv_html.py +++ b/src/wireviz/wv_html.py @@ -29,7 +29,6 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], '^<[?]xml [^?>]*[?]>[^<]*]*>', '', svgdata, 1) - html = html.replace('', svgdata) # generate BOM table bom = flatten2d(bom_list) @@ -54,33 +53,34 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], bom_html = '\n' + bom_header_html + ''.join(bom_contents) + '
\n' bom_html_reversed = '\n' + ''.join(list(reversed(bom_contents))) + bom_header_html + '
\n' - # insert BOM table - html = html.replace('', bom_html) - html = html.replace('', bom_html_reversed) - - # insert generator - html = html.replace('', f'{APP_NAME} {__version__} - {APP_URL}') - - # insert other metadata + # prepare simple replacements + replacements = { + '': f'{APP_NAME} {__version__} - {APP_URL}', + '': svgdata, + '': bom_html, + '': bom_html_reversed, + '': '1', # TODO: handle multi-page documents + '': '1', # TODO: handle multi-page documents + } + + # prepare metadata replacements if metadata: - - html = html.replace(f'"sheetsize_default"', '"{}"'.format(metadata.get('template',{}).get('sheetsize', ''))) # include quotes so no replacement happens within