#!/usr/bin/env python3 # -*- coding: utf-8 -*- import argparse import os from pathlib import Path import sys from typing import Any, Tuple import yaml if __name__ == '__main__': sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from wireviz import __version__ from wireviz.Harness import Harness from wireviz.wv_helper import expand, open_file_read def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, str, Tuple[str]) = None) -> Any: """ Parses yaml input string and does the high-level harness conversion :param yaml_input: a string containing the yaml input data :param file_out: :param return_types: if None, then returns None; if the value is a string, then a corresponding data format will be returned; if the value is a tuple of strings, then for every valid format in the `return_types` tuple, another return type will be generated and returned in the same order; currently supports: - "png" - will return the PNG data - "svg" - will return the SVG data - "harness" - will return the `Harness` instance """ yaml_data = yaml.safe_load(yaml_input) template_connectors = {} template_connector_names = [] template_cables = {} template_cable_names = [] designators_and_templates = {} autogenerated_designators = {} alternating_sections = ['connectors','cables'] harness = Harness() # add items 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 ty == dict: for key, attribs in yaml_data[sec].items(): # TODO: take care of this image thing # The Image dataclass might need to open an image file with a relative path. # image = attribs.get('image') # if isinstance(image, dict): # image['gv_dir'] = Path(file_out if file_out else '').parent # Inject context if sec == 'connectors': template_connectors[key] = attribs template_connector_names.append(key) elif sec == 'cables': template_cables[key] = attribs template_cable_names.append(key) else: pass # section exists but is empty else: # section does not exist, create empty section if ty == dict: yaml_data[sec] = {} elif ty == list: yaml_data[sec] = [] print('Conector templates:', template_connector_names) print('Cable templates: ', template_cable_names) def resolve_designator(inp): if '.' in inp: # generate a new instance of an item template, designator = inp.split('.') # TODO: handle more than one `.` if designator == '': autogenerated_designators[template] = autogenerated_designators.get(template, 0) + 1 designator = f'_{template}_{autogenerated_designators[template]}' # check if contradiction 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: template = inp designator = inp if designator in designators_and_templates: pass # referencing an exiting connector, no need to add again else: designators_and_templates[designator] = template return (template, designator) connection_sets = yaml_data['connections'] for connection_set in connection_sets: print('') print('connection set @0:', connection_set) # 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: connectioncount.append(None) # strings do not reveal connectioncount if not any(connectioncount): raise Exception('No item in connection set revealed number of connections') print(f'Connection count: {connectioncount}') # check that all entries are the same length if len(set(filter(None, connectioncount))) > 1: raise Exception('All items in connection set must reference the same number of connections') 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 print('connection set @1:', connection_set) print('des_temp @1:', designators_and_templates) # resolve all designators for index, entry in enumerate(connection_set): # print(index, ':') if isinstance(entry, list): for subindex, item in enumerate(entry): template, designator = resolve_designator(item) # print('list', index, subindex, item, template, designator) connection_set[index][subindex] = designator elif isinstance(entry, dict): key = list(entry.keys())[0] template, designator = resolve_designator(key) value = entry[key] # print('dict', key, template, designator, value) connection_set[index] = {designator: value} else: pass # string entries have been expanded in previous step print('connection set @2:', connection_set) print('des_temp @2:', designators_and_templates) # 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] print('connection set @3:', connection_set) # TODO: check alternating cable/connector # generate items for entry in connection_set: for item in entry: designator = list(item.keys())[0] template = designators_and_templates[designator] if designator in harness.connectors: print(' ', designator, 'is an existing connector instance') elif template in template_connector_names: print(' ', designator, 'is a new connector instance of type', template) harness.add_connector(name = designator, **template_connectors[template]) elif designator in harness.cables: print(' ', designator, 'is an existing cable instance') elif template in template_cable_names: print(' ', designator, 'is a new cable instance of type', template) harness.add_cable(name = designator, **template_cables[template]) else: print(f' Template {template} not found, neither in connectors nor in cables') quit() connections = [] for yaml_connection in yaml_data['connections']: print('connection set:') connection = [] for entry in yaml_connection: if isinstance(entry, list): itemlist = entry entrytype = 'list' elif isinstance(entry, dict): itemlist = [list(entry.keys())[0]] entrytype = 'dict' elif isinstance(entry, str): itemlist = [entry] entrytype = 'str' print(' ', itemlist) arrows = ['--', '<--', '<->', '-->','==', '<==', '<=>', '==>'] outputlist = [] for item in itemlist: if item in arrows: print(' ', item, 'arrow!') else: if '.' in item: # generate a new instance of an item template, designator = item.split('.') # TODO: handle more than one `.` if designator =='': autogenerated_designators[template] = autogenerated_designators.get(template, 0) + 1 designator = f'_{template}_{autogenerated_designators[template]}' print('new autogen id', designator, 'for', template) print(' ', template, '-->', designator) else: # use the item directly template = item designator = item print(' ', designator) if designator in harness.connectors: print(' ', designator, 'is an existing connector instance') elif template in template_connector_names: print(' ', designator, 'is a new connector instance of type', template) # print(template_connectors[template]) harness.add_connector(name = designator, **template_connectors[template]) # harness.connectors[designator] = templates.connectors[template] # harness.connectors[designator].name = designator elif designator in harness.cables: print(' ', designator, 'is an existing cable instance') elif template in template_cable_names: print(' ', designator, 'is a new cable instance of type', template) harness.add_cable(name = designator, **template_cables[template]) # harness.cables[designator] = templates.cables[template] # harness.cables[designator].name = designator else: print(f' {template} TEMPLATE not found, neither in connectors nor in cables') outputlist.append(designator) if not itemlist[0] in arrows: if entrytype == 'list': connection_entry = outputlist elif entrytype == 'dict': connection_entry = {outputlist[0]: list(entry.values())[0]} elif entrytype == 'str': connection_entry = outputlist[0] print(connection_entry) connection.append(connection_entry) print('connection', connection) # check which section the first item belongs to alternating_sections = ['connectors','cables'] alternating_section_keys = [list(section.keys()) for section in [harness.connectors, harness.cables]] print('alt_sec_keys', alternating_section_keys) for index, section in enumerate(alternating_section_keys): print('1st', first_item) if first_item in section: expected_index = index print(first_item, 'found in ', alternating_sections[index]) break 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 # 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_designators[item] = autogenerated_designators.get(item, 0) + 1 # new_id = f'_{item}_{autogenerated_designators[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_designators[subitem] = autogenerated_designators.get(subitem, 0) + 1 # new_id = f'_{subitem}_{autogenerated_designators[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') print('connection list') print(connection_list) print('') # 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] print('connect ', from_name, from_pin, via_name, via_pin, to_name, to_pin) harness.connect(from_name, from_pin, via_name, via_pin, to_name, to_pin) # quit () if "additional_bom_items" in yaml_data: for line in yaml_data["additional_bom_items"]: harness.add_bom_item(line) if file_out is not None: harness.output(filename=file_out, fmt=('png', 'svg'), view=False) if return_types is not None: returns = [] if isinstance(return_types, str): # only one return type speficied return_types = [return_types] return_types = [t.lower() for t in return_types] for rt in return_types: if rt == 'png': returns.append(harness.png) if rt == 'svg': returns.append(harness.svg) if rt == 'harness': returns.append(harness) return tuple(returns) if len(returns) != 1 else returns[0] def parse_file(yaml_file: str, file_out: (str, Path) = None) -> None: with open_file_read(yaml_file) as file: yaml_input = file.read() if not file_out: fn, fext = os.path.splitext(yaml_file) file_out = fn file_out = os.path.abspath(file_out) parse(yaml_input, file_out=file_out) def parse_cmdline(): parser = argparse.ArgumentParser( description='Generate cable and wiring harness documentation from YAML descriptions', ) parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('input_file', action='store', type=str, metavar='YAML_FILE') parser.add_argument('-o', '--output_file', action='store', type=str, metavar='OUTPUT') # Not implemented: parser.add_argument('--generate-bom', action='store_true', default=True) parser.add_argument('--prepend-file', action='store', type=str, metavar='YAML_FILE') return parser.parse_args() def main(): args = parse_cmdline() if not os.path.exists(args.input_file): print(f'Error: input file {args.input_file} inaccessible or does not exist, check path') sys.exit(1) with open_file_read(args.input_file) as fh: yaml_input = fh.read() if args.prepend_file: if not os.path.exists(args.prepend_file): print(f'Error: prepend input file {args.prepend_file} inaccessible or does not exist, check path') sys.exit(1) with open_file_read(args.prepend_file) as fh: prepend = fh.read() yaml_input = prepend + yaml_input if not args.output_file: file_out = args.input_file pre, _ = os.path.splitext(file_out) file_out = pre # extension will be added by graphviz output function else: file_out = args.output_file file_out = os.path.abspath(file_out) parse(yaml_input, file_out=file_out) if __name__ == '__main__': main()