Add Conduit support for cable bundling
- Add Conduit and ConduitConnector classes in DataClasses.py - Extend parsing in wireviz.py for conduits/conduit-connectors/conduit-connections - Add rendering logic in Harness.py (dotted style, port colors) - Update BOM in wv_bom.py - Add example ex16.yml Refs #485
This commit is contained in:
parent
e4fe099f8c
commit
bed9f43d04
46
examples/ex16.yml
Normal file
46
examples/ex16.yml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
connectors:
|
||||||
|
X1:
|
||||||
|
type: Connector
|
||||||
|
subtype: Male
|
||||||
|
pincount: 2
|
||||||
|
pins: [1, 2]
|
||||||
|
pinlabels: [A, B]
|
||||||
|
X2:
|
||||||
|
type: Connector
|
||||||
|
subtype: Female
|
||||||
|
pincount: 2
|
||||||
|
pins: [1, 2]
|
||||||
|
pinlabels: [A, B]
|
||||||
|
|
||||||
|
conduit-connectors:
|
||||||
|
X1:
|
||||||
|
type: Conduit Connector
|
||||||
|
X2:
|
||||||
|
type: Conduit Connector
|
||||||
|
|
||||||
|
cables:
|
||||||
|
W1:
|
||||||
|
wirecount: 1
|
||||||
|
colors: [BU]
|
||||||
|
W2:
|
||||||
|
wirecount: 1
|
||||||
|
colors: [BN]
|
||||||
|
|
||||||
|
conduits:
|
||||||
|
C1:
|
||||||
|
type: Conduit
|
||||||
|
ports: 2
|
||||||
|
colors: [BU, BN]
|
||||||
|
|
||||||
|
connections:
|
||||||
|
- - X1
|
||||||
|
- W1
|
||||||
|
- X2
|
||||||
|
- - X1
|
||||||
|
- W2
|
||||||
|
- X2
|
||||||
|
|
||||||
|
conduit-connections:
|
||||||
|
- - X1
|
||||||
|
- C1
|
||||||
|
- X2
|
||||||
@ -55,6 +55,7 @@ class Options:
|
|||||||
bgcolor_connector: Optional[Color] = None
|
bgcolor_connector: Optional[Color] = None
|
||||||
bgcolor_cable: Optional[Color] = None
|
bgcolor_cable: Optional[Color] = None
|
||||||
bgcolor_bundle: Optional[Color] = None
|
bgcolor_bundle: Optional[Color] = None
|
||||||
|
bgcolor_conduit: Optional[Color] = None
|
||||||
color_mode: ColorMode = "SHORT"
|
color_mode: ColorMode = "SHORT"
|
||||||
mini_bom_mode: bool = True
|
mini_bom_mode: bool = True
|
||||||
template_separator: str = "."
|
template_separator: str = "."
|
||||||
@ -68,6 +69,8 @@ class Options:
|
|||||||
self.bgcolor_cable = self.bgcolor_node
|
self.bgcolor_cable = self.bgcolor_node
|
||||||
if not self.bgcolor_bundle:
|
if not self.bgcolor_bundle:
|
||||||
self.bgcolor_bundle = self.bgcolor_cable
|
self.bgcolor_bundle = self.bgcolor_cable
|
||||||
|
if not self.bgcolor_conduit:
|
||||||
|
self.bgcolor_conduit = self.bgcolor_cable
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -165,6 +168,15 @@ class Connector:
|
|||||||
additional_components: List[AdditionalComponent] = field(default_factory=list)
|
additional_components: List[AdditionalComponent] = field(default_factory=list)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
if isinstance(self, ConduitConnector):
|
||||||
|
# ConduitConnectors don't need pins
|
||||||
|
if isinstance(self.image, dict):
|
||||||
|
self.image = Image(**self.image)
|
||||||
|
for i, item in enumerate(self.additional_components):
|
||||||
|
if isinstance(item, dict):
|
||||||
|
self.additional_components[i] = AdditionalComponent(**item)
|
||||||
|
return
|
||||||
|
|
||||||
if isinstance(self.image, dict):
|
if isinstance(self.image, dict):
|
||||||
self.image = Image(**self.image)
|
self.image = Image(**self.image)
|
||||||
|
|
||||||
@ -242,6 +254,11 @@ class Connector:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConduitConnector(Connector):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Cable:
|
class Cable:
|
||||||
name: Designator
|
name: Designator
|
||||||
@ -272,6 +289,7 @@ class Cable:
|
|||||||
show_wirenumbers: Optional[bool] = None
|
show_wirenumbers: Optional[bool] = None
|
||||||
ignore_in_bom: bool = False
|
ignore_in_bom: bool = False
|
||||||
additional_components: List[AdditionalComponent] = field(default_factory=list)
|
additional_components: List[AdditionalComponent] = field(default_factory=list)
|
||||||
|
conduits: List[str] = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if isinstance(self.image, dict):
|
if isinstance(self.image, dict):
|
||||||
@ -293,11 +311,11 @@ class Cable:
|
|||||||
if u.upper() == "AWG":
|
if u.upper() == "AWG":
|
||||||
self.gauge_unit = u.upper()
|
self.gauge_unit = u.upper()
|
||||||
else:
|
else:
|
||||||
self.gauge_unit = u.replace("mm2", "mm\u00B2")
|
self.gauge_unit = u.replace("mm2", "mm\u00b2")
|
||||||
|
|
||||||
elif self.gauge is not None: # gauge specified, assume mm2
|
elif self.gauge is not None: # gauge specified, assume mm2
|
||||||
if self.gauge_unit is None:
|
if self.gauge_unit is None:
|
||||||
self.gauge_unit = "mm\u00B2"
|
self.gauge_unit = "mm\u00b2"
|
||||||
else:
|
else:
|
||||||
pass # gauge not specified
|
pass # gauge not specified
|
||||||
|
|
||||||
@ -322,45 +340,54 @@ class Cable:
|
|||||||
|
|
||||||
self.connections = []
|
self.connections = []
|
||||||
|
|
||||||
if self.wirecount: # number of wires explicitly defined
|
if not isinstance(self, Conduit):
|
||||||
if self.colors: # use custom color palette (partly or looped if needed)
|
if self.wirecount: # number of wires explicitly defined
|
||||||
pass
|
if self.colors: # use custom color palette (partly or looped if needed)
|
||||||
elif self.color_code:
|
pass
|
||||||
# use standard color palette (partly or looped if needed)
|
elif self.color_code:
|
||||||
if self.color_code not in COLOR_CODES:
|
# use standard color palette (partly or looped if needed)
|
||||||
raise Exception("Unknown color code")
|
if self.color_code not in COLOR_CODES:
|
||||||
self.colors = COLOR_CODES[self.color_code]
|
raise Exception("Unknown color code")
|
||||||
else: # no colors defined, add dummy colors
|
self.colors = COLOR_CODES[self.color_code]
|
||||||
self.colors = [""] * self.wirecount
|
else: # no colors defined, add dummy colors
|
||||||
|
self.colors = [""] * self.wirecount
|
||||||
|
|
||||||
# make color code loop around if more wires than colors
|
# make color code loop around if more wires than colors
|
||||||
if self.wirecount > len(self.colors):
|
if self.wirecount > len(self.colors):
|
||||||
m = self.wirecount // len(self.colors) + 1
|
m = self.wirecount // len(self.colors) + 1
|
||||||
self.colors = self.colors * int(m)
|
self.colors = self.colors * int(m)
|
||||||
# cut off excess after looping
|
# cut off excess after looping
|
||||||
self.colors = self.colors[: self.wirecount]
|
self.colors = self.colors[: self.wirecount]
|
||||||
else: # wirecount implicit in length of color list
|
else: # wirecount implicit in length of color list
|
||||||
if not self.colors:
|
if not self.colors:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Unknown number of wires. Must specify wirecount or colors (implicit length)"
|
"Unknown number of wires. Must specify wirecount or colors (implicit length)"
|
||||||
)
|
)
|
||||||
self.wirecount = len(self.colors)
|
self.wirecount = len(self.colors)
|
||||||
|
|
||||||
if self.wirelabels:
|
if self.wirelabels:
|
||||||
if self.shield and "s" in self.wirelabels:
|
if self.shield and "s" in self.wirelabels:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'"s" may not be used as a wire label for a shielded cable.'
|
'"s" may not be used as a wire label for a shielded cable.'
|
||||||
)
|
)
|
||||||
|
|
||||||
# if lists of part numbers are provided check this is a bundle and that it matches the wirecount.
|
# if lists of part numbers are provided check this is a bundle and that it matches the wirecount.
|
||||||
for idfield in [self.manufacturer, self.mpn, self.supplier, self.spn, self.pn]:
|
for idfield in [
|
||||||
if isinstance(idfield, list):
|
self.manufacturer,
|
||||||
if self.category == "bundle":
|
self.mpn,
|
||||||
# check the length
|
self.supplier,
|
||||||
if len(idfield) != self.wirecount:
|
self.spn,
|
||||||
raise Exception("lists of part data must match wirecount")
|
self.pn,
|
||||||
else:
|
]:
|
||||||
raise Exception("lists of part data are only supported for bundles")
|
if isinstance(idfield, list):
|
||||||
|
if self.category == "bundle":
|
||||||
|
# check the length
|
||||||
|
if len(idfield) != self.wirecount:
|
||||||
|
raise Exception("lists of part data must match wirecount")
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
"lists of part data are only supported for bundles"
|
||||||
|
)
|
||||||
|
|
||||||
if self.show_name is None:
|
if self.show_name is None:
|
||||||
# hide designators for auto-generated cables by default
|
# hide designators for auto-generated cables by default
|
||||||
@ -410,6 +437,33 @@ class Cable:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Conduit(Cable):
|
||||||
|
cables: List[Cable] = field(default_factory=list)
|
||||||
|
ports: int = 0
|
||||||
|
cableports: dict[str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def get_port(self, cable: Cable, port: int) -> int:
|
||||||
|
conduit_port = None
|
||||||
|
if cable.name in self.cableports:
|
||||||
|
if len(self.cableports[cable.name]) >= port:
|
||||||
|
conduit_port = self.cableports[cable.name][port - 1]
|
||||||
|
else:
|
||||||
|
self.cableports[cable.name] = []
|
||||||
|
|
||||||
|
if conduit_port:
|
||||||
|
return conduit_port
|
||||||
|
else:
|
||||||
|
self.cableports[cable.name].extend(
|
||||||
|
[0] * (port - len(self.cableports[cable.name]))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ports = self.ports + 1
|
||||||
|
self.cableports[cable.name][port - 1] = self.ports
|
||||||
|
self.colors.append(cable.colors[port - 1])
|
||||||
|
return self.ports
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Connection:
|
class Connection:
|
||||||
from_name: Optional[Designator]
|
from_name: Optional[Designator]
|
||||||
|
|||||||
@ -8,9 +8,12 @@ from pathlib import Path
|
|||||||
from typing import Any, List, Union
|
from typing import Any, List, Union
|
||||||
|
|
||||||
from graphviz import Graph
|
from graphviz import Graph
|
||||||
|
|
||||||
from wireviz import APP_NAME, APP_URL, __version__, wv_colors
|
from wireviz import APP_NAME, APP_URL, __version__, wv_colors
|
||||||
from wireviz.DataClasses import (
|
from wireviz.DataClasses import (
|
||||||
Cable,
|
Cable,
|
||||||
|
Conduit,
|
||||||
|
ConduitConnector,
|
||||||
Connector,
|
Connector,
|
||||||
MateComponent,
|
MateComponent,
|
||||||
MatePin,
|
MatePin,
|
||||||
@ -73,7 +76,9 @@ class Harness:
|
|||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.connectors = {}
|
self.connectors = {}
|
||||||
|
self.conduit_connectors = {}
|
||||||
self.cables = {}
|
self.cables = {}
|
||||||
|
self.conduits = {}
|
||||||
self.mates = []
|
self.mates = []
|
||||||
self._bom = [] # Internal Cache for generated bom
|
self._bom = [] # Internal Cache for generated bom
|
||||||
self.additional_bom_items = []
|
self.additional_bom_items = []
|
||||||
@ -82,9 +87,15 @@ class Harness:
|
|||||||
check_old(f"Connector '{name}'", OLD_CONNECTOR_ATTR, kwargs)
|
check_old(f"Connector '{name}'", OLD_CONNECTOR_ATTR, kwargs)
|
||||||
self.connectors[name] = Connector(name, *args, **kwargs)
|
self.connectors[name] = Connector(name, *args, **kwargs)
|
||||||
|
|
||||||
|
def add_conduit_connector(self, name: str, *args, **kwargs) -> None:
|
||||||
|
self.conduit_connectors[name] = ConduitConnector(name, *args, **kwargs)
|
||||||
|
|
||||||
def add_cable(self, name: str, *args, **kwargs) -> None:
|
def add_cable(self, name: str, *args, **kwargs) -> None:
|
||||||
self.cables[name] = Cable(name, *args, **kwargs)
|
self.cables[name] = Cable(name, *args, **kwargs)
|
||||||
|
|
||||||
|
def add_conduit(self, name: str, *args, **kwargs) -> None:
|
||||||
|
self.conduits[name] = Conduit(name, *args, **kwargs)
|
||||||
|
|
||||||
def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_type) -> None:
|
def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_type) -> None:
|
||||||
self.mates.append(MatePin(from_name, from_pin, to_name, to_pin, 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[from_name].activate_pin(from_pin, Side.RIGHT)
|
||||||
@ -100,6 +111,7 @@ class Harness:
|
|||||||
self,
|
self,
|
||||||
from_name: str,
|
from_name: str,
|
||||||
from_pin: (int, str),
|
from_pin: (int, str),
|
||||||
|
conduits: [str],
|
||||||
via_name: str,
|
via_name: str,
|
||||||
via_wire: (int, str),
|
via_wire: (int, str),
|
||||||
to_name: str,
|
to_name: str,
|
||||||
@ -128,7 +140,7 @@ class Harness:
|
|||||||
if not pin in connector.pins:
|
if not pin in connector.pins:
|
||||||
raise Exception(f"{name}:{pin} not found.")
|
raise Exception(f"{name}:{pin} not found.")
|
||||||
|
|
||||||
# check via cable
|
# check via cable or conduit
|
||||||
if via_name in self.cables:
|
if via_name in self.cables:
|
||||||
cable = self.cables[via_name]
|
cable = self.cables[via_name]
|
||||||
# check if provided name is ambiguous
|
# check if provided name is ambiguous
|
||||||
@ -153,9 +165,27 @@ class Harness:
|
|||||||
via_wire = (
|
via_wire = (
|
||||||
cable.wirelabels.index(via_wire) + 1
|
cable.wirelabels.index(via_wire) + 1
|
||||||
) # list index starts at 0, wire IDs start at 1
|
) # list index starts at 0, wire IDs start at 1
|
||||||
|
cable.conduits = conduits
|
||||||
|
elif via_name in self.conduits:
|
||||||
|
conduit = self.conduits[via_name]
|
||||||
|
# for conduits, via_wire is the port number
|
||||||
|
if not isinstance(via_wire, int):
|
||||||
|
raise Exception(
|
||||||
|
f"{via_name}:{via_wire} must be an integer port number for conduits."
|
||||||
|
)
|
||||||
|
if via_wire < 1 or via_wire > conduit.ports:
|
||||||
|
raise Exception(f"{via_name}:{via_wire} port out of range.")
|
||||||
|
conduit.conduits = conduits
|
||||||
|
|
||||||
# perform the actual connection
|
# perform the actual connection
|
||||||
self.cables[via_name].connect(from_name, from_pin, via_wire, to_name, to_pin)
|
if via_name in self.cables:
|
||||||
|
self.cables[via_name].connect(
|
||||||
|
from_name, from_pin, via_wire, to_name, to_pin
|
||||||
|
)
|
||||||
|
elif via_name in self.conduits:
|
||||||
|
self.conduits[via_name].connect(
|
||||||
|
from_name, from_pin, via_wire, to_name, to_pin
|
||||||
|
)
|
||||||
if from_name in self.connectors:
|
if from_name in self.connectors:
|
||||||
self.connectors[from_name].activate_pin(from_pin, Side.RIGHT)
|
self.connectors[from_name].activate_pin(from_pin, Side.RIGHT)
|
||||||
if to_name in self.connectors:
|
if to_name in self.connectors:
|
||||||
@ -302,10 +332,10 @@ class Harness:
|
|||||||
# Only convert units we actually know about, i.e. currently
|
# Only convert units we actually know about, i.e. currently
|
||||||
# mm2 and awg --- other units _are_ technically allowed,
|
# mm2 and awg --- other units _are_ technically allowed,
|
||||||
# and passed through as-is.
|
# and passed through as-is.
|
||||||
if cable.gauge_unit == "mm\u00B2":
|
if cable.gauge_unit == "mm\u00b2":
|
||||||
awg_fmt = f" ({awg_equiv(cable.gauge)} AWG)"
|
awg_fmt = f" ({awg_equiv(cable.gauge)} AWG)"
|
||||||
elif cable.gauge_unit.upper() == "AWG":
|
elif cable.gauge_unit.upper() == "AWG":
|
||||||
awg_fmt = f" ({mm2_equiv(cable.gauge)} mm\u00B2)"
|
awg_fmt = f" ({mm2_equiv(cable.gauge)} mm\u00b2)"
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}'
|
rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}'
|
||||||
@ -532,6 +562,79 @@ class Harness:
|
|||||||
fillcolor=translate_color(bgcolor, "HEX"),
|
fillcolor=translate_color(bgcolor, "HEX"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for conduit in self.conduits.values():
|
||||||
|
html = []
|
||||||
|
|
||||||
|
awg_fmt = ""
|
||||||
|
if conduit.show_equiv:
|
||||||
|
# Only convert units we actually know about, i.e. currently
|
||||||
|
# mm2 and awg --- other units _are_ technically allowed,
|
||||||
|
# and passed through as-is.
|
||||||
|
if conduit.gauge_unit == "mm\u00b2":
|
||||||
|
awg_fmt = f" ({awg_equiv(conduit.gauge)} AWG)"
|
||||||
|
elif conduit.gauge_unit.upper() == "AWG":
|
||||||
|
awg_fmt = f" ({mm2_equiv(conduit.gauge)} mm\u00b2)"
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
rows = [[f'{html_bgcolor(conduit.bgcolor_title)}{remove_links(conduit.name)}'
|
||||||
|
if conduit.show_name else None],
|
||||||
|
[pn_info_string(HEADER_PN, None,
|
||||||
|
remove_links(conduit.pn)) if not isinstance(conduit.pn, list) else None,
|
||||||
|
html_line_breaks(pn_info_string(HEADER_MPN,
|
||||||
|
conduit.manufacturer if not isinstance(conduit.manufacturer, list) else None,
|
||||||
|
conduit.mpn if not isinstance(conduit.mpn, list) else None)),
|
||||||
|
html_line_breaks(pn_info_string(HEADER_SPN,
|
||||||
|
conduit.supplier if not isinstance(conduit.supplier, list) else None,
|
||||||
|
conduit.spn if not isinstance(conduit.spn, list) else None))],
|
||||||
|
[html_line_breaks(conduit.type),
|
||||||
|
f'{conduit.gauge} {conduit.gauge_unit}{awg_fmt}' if conduit.gauge else None,
|
||||||
|
f'{conduit.length} {conduit.length_unit}' if conduit.length > 0 else None,
|
||||||
|
translate_color(conduit.color, self.options.color_mode) if conduit.color else None,
|
||||||
|
html_colorbar(cable.color)],
|
||||||
|
'<!-- wire table -->',
|
||||||
|
[html_image(conduit.image)],
|
||||||
|
[html_caption(conduit.image)]]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
rows.extend(get_additional_component_table(self, conduit))
|
||||||
|
rows.append([html_line_breaks(conduit.notes)])
|
||||||
|
html.extend(nested_html_table(rows, html_bgcolor_attr(conduit.bgcolor)))
|
||||||
|
|
||||||
|
wirehtml = []
|
||||||
|
# conductor table
|
||||||
|
wirehtml.append('<table border="0" cellspacing="0" cellborder="0">')
|
||||||
|
wirehtml.append(" <tr><td> </td></tr>")
|
||||||
|
|
||||||
|
for i in range(1, conduit.ports + 1):
|
||||||
|
# fmt: off
|
||||||
|
bgcolors = ['#000000'] + get_color_hex(conduit.colors[i - 1], pad=pad) + ['#000000']
|
||||||
|
wirehtml.append(f" <tr>")
|
||||||
|
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">')
|
||||||
|
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">')
|
||||||
|
for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors
|
||||||
|
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>')
|
||||||
|
wirehtml.append(" </table>")
|
||||||
|
wirehtml.append(" </td>")
|
||||||
|
wirehtml.append(" </tr>")
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
wirehtml.append(" <tr><td> </td></tr>")
|
||||||
|
|
||||||
|
wirehtml.append(" </table>")
|
||||||
|
|
||||||
|
html = [
|
||||||
|
row.replace("<!-- wire table -->", "\n".join(wirehtml)) for row in html
|
||||||
|
]
|
||||||
|
|
||||||
|
html = "\n".join(html)
|
||||||
|
dot.node(
|
||||||
|
conduit.name,
|
||||||
|
label=f"<\n{html}\n>",
|
||||||
|
shape="box",
|
||||||
|
style="dotted",
|
||||||
|
fillcolor=translate_color(self.options.bgcolor_conduit, "HEX"),
|
||||||
|
)
|
||||||
|
|
||||||
# mates
|
# mates
|
||||||
for mate in self.mates:
|
for mate in self.mates:
|
||||||
if mate.shape[-1] == ">":
|
if mate.shape[-1] == ">":
|
||||||
|
|||||||
@ -109,7 +109,10 @@ def parse(
|
|||||||
# containers for parsed component data and connection sets
|
# containers for parsed component data and connection sets
|
||||||
template_connectors = {}
|
template_connectors = {}
|
||||||
template_cables = {}
|
template_cables = {}
|
||||||
|
template_conduits = {}
|
||||||
|
template_conduit_connectors = {}
|
||||||
connection_sets = []
|
connection_sets = []
|
||||||
|
conduit_connection_sets = []
|
||||||
# actual harness
|
# actual harness
|
||||||
harness = Harness(
|
harness = Harness(
|
||||||
metadata=Metadata(**yaml_data.get("metadata", {})),
|
metadata=Metadata(**yaml_data.get("metadata", {})),
|
||||||
@ -129,8 +132,15 @@ def parse(
|
|||||||
# add items
|
# add items
|
||||||
# parse YAML input file ====================================================
|
# parse YAML input file ====================================================
|
||||||
|
|
||||||
sections = ["connectors", "cables", "connections"]
|
sections = [
|
||||||
types = [dict, dict, list]
|
"connectors",
|
||||||
|
"cables",
|
||||||
|
"conduits",
|
||||||
|
"conduit-connectors",
|
||||||
|
"connections",
|
||||||
|
"conduit-connections",
|
||||||
|
]
|
||||||
|
types = [dict, dict, dict, dict, list, list]
|
||||||
for sec, ty in zip(sections, types):
|
for sec, ty in zip(sections, types):
|
||||||
if sec in yaml_data and type(yaml_data[sec]) == ty: # section exists
|
if sec in yaml_data and type(yaml_data[sec]) == ty: # section exists
|
||||||
if len(yaml_data[sec]) > 0: # section has contents
|
if len(yaml_data[sec]) > 0: # section has contents
|
||||||
@ -149,6 +159,10 @@ def parse(
|
|||||||
template_connectors[key] = attribs
|
template_connectors[key] = attribs
|
||||||
elif sec == "cables":
|
elif sec == "cables":
|
||||||
template_cables[key] = attribs
|
template_cables[key] = attribs
|
||||||
|
elif sec == "conduits":
|
||||||
|
template_conduits[key] = attribs
|
||||||
|
elif sec == "conduit-connectors":
|
||||||
|
template_conduit_connectors[key] = attribs
|
||||||
else: # section exists but is empty
|
else: # section exists but is empty
|
||||||
pass
|
pass
|
||||||
else: # section does not exist, create empty section
|
else: # section does not exist, create empty section
|
||||||
@ -158,6 +172,8 @@ def parse(
|
|||||||
yaml_data[sec] = []
|
yaml_data[sec] = []
|
||||||
|
|
||||||
connection_sets = yaml_data["connections"]
|
connection_sets = yaml_data["connections"]
|
||||||
|
conduit_connection_sets = yaml_data.get("conduit-connections", [])
|
||||||
|
conduit_dict = {}
|
||||||
|
|
||||||
# go through connection sets, generate and connect components ==============
|
# go through connection sets, generate and connect components ==============
|
||||||
|
|
||||||
@ -192,6 +208,7 @@ def parse(
|
|||||||
# utilities to check for alternating connectors and cables/arrows ==========
|
# utilities to check for alternating connectors and cables/arrows ==========
|
||||||
|
|
||||||
alternating_types = ["connector", "cable/arrow"]
|
alternating_types = ["connector", "cable/arrow"]
|
||||||
|
alternating_types_conduit = ["conduit-connector", "conduit"]
|
||||||
expected_type = None
|
expected_type = None
|
||||||
|
|
||||||
def check_type(designator, template, actual_type):
|
def check_type(designator, template, actual_type):
|
||||||
@ -204,7 +221,7 @@ def parse(
|
|||||||
f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}'
|
f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def alternate_type(): # flip between connector and cable/arrow
|
def alternate_type(alternating_types): # flip between types
|
||||||
nonlocal expected_type
|
nonlocal expected_type
|
||||||
expected_type = alternating_types[1 - alternating_types.index(expected_type)]
|
expected_type = alternating_types[1 - alternating_types.index(expected_type)]
|
||||||
|
|
||||||
@ -307,7 +324,9 @@ def parse(
|
|||||||
f"{template} is an unknown template/designator/arrow."
|
f"{template} is an unknown template/designator/arrow."
|
||||||
)
|
)
|
||||||
|
|
||||||
alternate_type() # entries in connection set must alternate between connectors and cables/arrows
|
alternate_type(
|
||||||
|
alternating_types
|
||||||
|
) # entries in connection set must alternate between connectors and cables/arrows
|
||||||
|
|
||||||
# transpose connection set list
|
# transpose connection set list
|
||||||
# before: one item per component, one subitem per connection in set
|
# before: one item per component, one subitem per connection in set
|
||||||
@ -336,7 +355,7 @@ def parse(
|
|||||||
entry[index_item + 1]
|
entry[index_item + 1]
|
||||||
)
|
)
|
||||||
harness.connect(
|
harness.connect(
|
||||||
from_name, from_pin, via_name, via_pin, to_name, to_pin
|
from_name, from_pin, [], via_name, via_pin, to_name, to_pin
|
||||||
)
|
)
|
||||||
|
|
||||||
elif is_arrow(designator):
|
elif is_arrow(designator):
|
||||||
@ -362,10 +381,172 @@ def parse(
|
|||||||
# mate two connectors as a whole
|
# mate two connectors as a whole
|
||||||
harness.add_mate_component(from_name, to_name, designator)
|
harness.add_mate_component(from_name, to_name, designator)
|
||||||
|
|
||||||
|
# go through conduit connection sets, generate and connect conduit components ==============
|
||||||
|
|
||||||
|
for connection_set in conduit_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])))
|
||||||
|
# e.g.: - X1: [1-4,6] yields 5
|
||||||
|
else:
|
||||||
|
pass # strings do not reveal connectioncount
|
||||||
|
if not any(connectioncount):
|
||||||
|
# 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
|
||||||
|
# - ==>
|
||||||
|
# - 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.conduit_connectors
|
||||||
|
): # existing conduit connector instance
|
||||||
|
check_type(designator, template, "conduit-connector")
|
||||||
|
elif template in template_conduit_connectors.keys():
|
||||||
|
# generate new conduit connector instance from template
|
||||||
|
check_type(designator, template, "conduit-connector")
|
||||||
|
harness.add_conduit_connector(
|
||||||
|
name=designator, **template_conduit_connectors[template]
|
||||||
|
)
|
||||||
|
|
||||||
|
elif designator in harness.conduits: # existing conduit instance
|
||||||
|
check_type(designator, template, "conduit")
|
||||||
|
elif template in template_conduits.keys():
|
||||||
|
# generate new conduit instance from template
|
||||||
|
check_type(designator, template, "conduit")
|
||||||
|
harness.add_conduit(name=designator, **template_conduits[template])
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
f"{template} is an unknown conduit template/designator."
|
||||||
|
)
|
||||||
|
|
||||||
|
alternate_type(
|
||||||
|
alternating_types_conduit
|
||||||
|
) # entries in connection set must alternate between conduit-connectors and conduits
|
||||||
|
|
||||||
|
# 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 conduit 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.conduits:
|
||||||
|
if index_item == 0:
|
||||||
|
# list started with a conduit, no conduit connector to join on left side
|
||||||
|
from_name, from_pin = (None, None)
|
||||||
|
else:
|
||||||
|
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 conduit, no conduit connector to join on right side
|
||||||
|
to_name, to_pin = (None, None)
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# build conduit_dict
|
||||||
|
for conduit_connection_set in conduit_connection_sets:
|
||||||
|
for connection in conduit_connection_set:
|
||||||
|
if len(connection) == 3:
|
||||||
|
from_name = connection[0]
|
||||||
|
via_name = connection[1]
|
||||||
|
to_name = connection[2]
|
||||||
|
if via_name in harness.conduits:
|
||||||
|
conduit = via_name
|
||||||
|
conduit_connectors = [from_name, to_name]
|
||||||
|
for cable_name, cable in harness.cables.items():
|
||||||
|
for conn in cable.connections:
|
||||||
|
if (
|
||||||
|
conn.from_name in conduit_connectors
|
||||||
|
or conn.to_name in conduit_connectors
|
||||||
|
):
|
||||||
|
if cable_name not in conduit_dict:
|
||||||
|
conduit_dict[cable_name] = []
|
||||||
|
if conduit not in conduit_dict[cable_name]:
|
||||||
|
conduit_dict[cable_name].append(conduit)
|
||||||
|
|
||||||
|
# set conduits for cables
|
||||||
|
for cable_name, cable in harness.cables.items():
|
||||||
|
cable.conduits = conduit_dict.get(cable_name, [])
|
||||||
|
|
||||||
# warn about unused templates
|
# warn about unused templates
|
||||||
|
|
||||||
proposed_components = list(template_connectors.keys()) + list(
|
proposed_components = (
|
||||||
template_cables.keys()
|
list(template_connectors.keys())
|
||||||
|
+ list(template_cables.keys())
|
||||||
|
+ list(template_conduits.keys())
|
||||||
|
+ list(template_conduit_connectors.keys())
|
||||||
)
|
)
|
||||||
used_components = set(designators_and_templates.values())
|
used_components = set(designators_and_templates.values())
|
||||||
forgotten_components = [c for c in proposed_components if not c in used_components]
|
forgotten_components = [c for c in proposed_components if not c in used_components]
|
||||||
|
|||||||
@ -185,6 +185,56 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
|
|||||||
# add cable/bundles aditional components to bom
|
# add cable/bundles aditional components to bom
|
||||||
bom_entries.extend(get_additional_component_bom(cable))
|
bom_entries.extend(get_additional_component_bom(cable))
|
||||||
|
|
||||||
|
# conduits
|
||||||
|
for conduit in harness.conduits.values():
|
||||||
|
if not conduit.ignore_in_bom:
|
||||||
|
description = (
|
||||||
|
"Conduit"
|
||||||
|
+ (f", {conduit.type}" if conduit.type else "")
|
||||||
|
+ (f", {conduit.gauge} {conduit.gauge_unit}" if conduit.gauge else "")
|
||||||
|
+ (
|
||||||
|
f", {conduit.length} {conduit.length_unit}"
|
||||||
|
if conduit.length > 0
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
f", {translate_color(conduit.color, harness.options.color_mode)}"
|
||||||
|
if conduit.color
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bom_entries.append(
|
||||||
|
{
|
||||||
|
"description": description,
|
||||||
|
"qty": conduit.length,
|
||||||
|
"unit": conduit.length_unit,
|
||||||
|
"designators": conduit.name if conduit.show_name else None,
|
||||||
|
**optional_fields(conduit),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# add conduits aditional components to bom
|
||||||
|
bom_entries.extend(get_additional_component_bom(conduit))
|
||||||
|
|
||||||
|
# conduit connectors
|
||||||
|
for conduit_connector in harness.conduit_connectors.values():
|
||||||
|
if not conduit_connector.ignore_in_bom:
|
||||||
|
description = "Conduit Connector" + (
|
||||||
|
f", {conduit_connector.type}" if conduit_connector.type else ""
|
||||||
|
)
|
||||||
|
bom_entries.append(
|
||||||
|
{
|
||||||
|
"description": description,
|
||||||
|
"designators": (
|
||||||
|
conduit_connector.name if conduit_connector.show_name else None
|
||||||
|
),
|
||||||
|
**optional_fields(conduit_connector),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# add conduit connectors aditional components to bom
|
||||||
|
bom_entries.extend(get_additional_component_bom(conduit_connector))
|
||||||
|
|
||||||
# add harness aditional components to bom directly, as they both are List[BOMEntry]
|
# add harness aditional components to bom directly, as they both are List[BOMEntry]
|
||||||
bom_entries.extend(harness.additional_bom_items)
|
bom_entries.extend(harness.additional_bom_items)
|
||||||
|
|
||||||
@ -204,9 +254,11 @@ def generate_bom(harness: "Harness") -> List[BOMEntry]:
|
|||||||
bom.append(
|
bom.append(
|
||||||
{
|
{
|
||||||
**group_entries[0],
|
**group_entries[0],
|
||||||
"qty": int(total_qty)
|
"qty": (
|
||||||
if float(total_qty).is_integer()
|
int(total_qty)
|
||||||
else round(total_qty, 3),
|
if float(total_qty).is_integer()
|
||||||
|
else round(total_qty, 3)
|
||||||
|
),
|
||||||
"designators": sorted(set(designators)),
|
"designators": sorted(set(designators)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -111,9 +111,9 @@ def generate_html_output(
|
|||||||
if isinstance(entry, Dict):
|
if isinstance(entry, Dict):
|
||||||
replacements[f"<!-- %{item}_{index+1}% -->"] = str(category)
|
replacements[f"<!-- %{item}_{index+1}% -->"] = str(category)
|
||||||
for entry_key, entry_value in entry.items():
|
for entry_key, entry_value in entry.items():
|
||||||
replacements[
|
replacements[f"<!-- %{item}_{index+1}_{entry_key}% -->"] = (
|
||||||
f"<!-- %{item}_{index+1}_{entry_key}% -->"
|
html_line_breaks(str(entry_value))
|
||||||
] = html_line_breaks(str(entry_value))
|
)
|
||||||
elif isinstance(entry, (str, int, float)):
|
elif isinstance(entry, (str, int, float)):
|
||||||
pass # TODO?: replacements[f"<!-- %{item}_{category}% -->"] = html_line_breaks(str(entry))
|
pass # TODO?: replacements[f"<!-- %{item}_{category}% -->"] = html_line_breaks(str(entry))
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user