diff --git a/examples/demo02.yml b/examples/demo02.yml index 00ee5546..600b1359 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/ex04.yml b/examples/ex04.yml index b9eb7079..3fe5beda 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/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 44694174..0e82492f 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -24,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: @@ -54,11 +55,12 @@ def __post_init__(self): if len(self.pinnumbers) != len(set(self.pinnumbers)): raise Exception('Pin numbers are not unique') - 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 + 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 diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index bcc3d28c..a5e5d6ee 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -22,9 +22,6 @@ def add_connector(self, name, *args, **kwargs): def add_cable(self, name, *args, **kwargs): self.cables[name] = Cable(name, *args, **kwargs) - def loop(self, connector_name, from_pin, to_pin): - self.connectors[connector_name].loop(from_pin, to_pin) - def connect(self, from_name, from_pin, via_name, via_pin, to_name, to_pin): for (name, pin) in zip([from_name, to_name], [from_pin, to_pin]): # check from and to connectors @@ -46,7 +43,6 @@ def connect(self, from_name, from_pin, via_name, via_pin, to_name, to_pin): from_pin = pin if name == to_name: to_pin = pin - # TODO: what happens with loops? if not pin in connector.pinnumbers: raise Exception(f'{name}:{pin} not found.') diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index 465b3899..672dbdd4 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -13,6 +13,7 @@ from wireviz.Harness import Harness +from wireviz.wv_helper import expand def parse(yaml_input, file_out=None, generate_bom=False, return_types: (None, str, Tuple[str]) = None): @@ -31,40 +32,6 @@ def parse(yaml_input, file_out=None, generate_bom=False, return_types: (None, st 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 @@ -74,11 +41,12 @@ def check_designators(what, where): 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: @@ -90,108 +58,117 @@ def check_designators(what, where): 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') - - 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) + 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 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 + + # 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 file_out is not None: harness.output(filename=file_out, fmt=('png', 'svg'), gen_bom=generate_bom, view=False) diff --git a/src/wireviz/wv_helper.py b/src/wireviz/wv_helper.py index 83ee46ee..9c19a5e4 100644 --- a/src/wireviz/wv_helper.py +++ b/src/wireviz/wv_helper.py @@ -59,6 +59,35 @@ def nested_html_table(rows): 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: + try: + x = int(e) + except Exception: + x = e + output.append(x) + return output + + def int2tuple(inp): if isinstance(inp, tuple): output = inp diff --git a/tutorial/readme.md b/tutorial/readme.md index c46011c2..b82d7cb9 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 diff --git a/tutorial/tutorial05.yml b/tutorial/tutorial05.yml index e238f309..dda905ed 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 f69499c3..bcf9c8c4 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