Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions examples/ex17_loop_labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Connector loops using pin labels
# RS-232 loopback adapter: RTS-CTS and DSR-DTR-DCD jumpered,
# with TX/RX/GND passed through to a cable.

connectors:
X1:
type: D-Sub
subtype: female
pinlabels: [DCD, RX, TX, DTR, GND, DSR, RTS, CTS, RI]
loops:
- [RTS, CTS] # pin labels instead of numbers
- [DSR, DTR]
- [4, DCD] # mixed: pin number + label

cables:
W1:
wirecount: 3
colors: [BK, RD, GN]

connections:
-
- X1: [RX, TX, GND] # labels in connections too
- W1: [1-3]
83 changes: 75 additions & 8 deletions src/wireviz/DataClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Dict, List, Optional, Tuple, Union

from wireviz.wv_colors import COLOR_CODES, Color, ColorMode, Colors, ColorScheme
from wireviz.wv_helper import aspect_ratio, int2tuple
from wireviz.wv_helper import aspect_ratio, int2tuple, normalize_pin

# Each type alias have their legal values described in comments - validation might be implemented in the future
PlainText = str # Text not containing HTML tags nor newlines
Expand Down Expand Up @@ -168,6 +168,15 @@ def __post_init__(self) -> None:
if isinstance(self.image, dict):
self.image = Image(**self.image)

# Normalize pin-like fields so int/str types are consistent
# regardless of YAML quoting (e.g. "1" vs 1).
if self.pins:
self.pins = [normalize_pin(p) for p in self.pins]
if self.pinlabels:
self.pinlabels = [normalize_pin(p) for p in self.pinlabels]
if self.loops:
self.loops = [[normalize_pin(p) for p in loop] for loop in self.loops]

self.ports_left = False
self.ports_right = False
self.visible_pins = {}
Expand Down Expand Up @@ -203,24 +212,81 @@ def __post_init__(self) -> None:
# hide pincount for simple (1 pin) connectors by default
self.show_pincount = self.style != "simple"

for loop in self.loops:
# TODO: allow using pin labels in addition to pin numbers, just like when defining regular connections
for i, loop in enumerate(self.loops):
# TODO: include properties of wire used to create the loop
if len(loop) != 2:
raise Exception("Loops must be between exactly two pins!")
resolved = []
for pin in loop:
if pin not in self.pins:
raise Exception(
f'Unknown loop pin "{pin}" for connector "{self.name}"!'
)
pin = self.resolve_pin(pin)
resolved.append(pin)
# Make sure loop connected pins are not hidden.
self.activate_pin(pin, None)
if resolved[0] == resolved[1]:
raise Exception(
f'Loop in connector "{self.name}" connects pin '
f'"{resolved[0]}" to itself.'
)
self.loops[i] = resolved

for i, item in enumerate(self.additional_components):
if isinstance(item, dict):
self.additional_components[i] = AdditionalComponent(**item)

def activate_pin(self, pin: Pin, side: Side) -> None:
def resolve_pin(self, pin: Pin) -> Pin:
"""Resolve a pin identifier to its canonical pin number.

Given a value that may be either a pin number (from self.pins)
or a pin label (from self.pinlabels), returns the corresponding
pin number from self.pins.

Callers needing a positional index should use
self.pins.index(return_value).

Resolution order:
1. Value in both pins and pinlabels at the same position
-> return directly (no ambiguity).
2. Value in both at different positions -> raise.
3. Value only in pinlabels -> return corresponding pin number.
4. Value only in pins -> return directly.
5. Not found -> raise.

Note: Lookups are type-sensitive (int 1 != str "1").
"""
in_pins = pin in self.pins
in_labels = pin in self.pinlabels if self.pinlabels else False

if in_pins and in_labels:
# present in both lists — check for duplicate labels first
if self.pinlabels.count(pin) > 1:
raise Exception(
f'Pin label "{pin}" in connector "{self.name}" '
f"is defined more than once in pinlabels."
)
# then check for positional ambiguity
if self.pins.index(pin) != self.pinlabels.index(pin):
raise Exception(
f'"{pin}" in connector "{self.name}" exists in both '
f"pins and pinlabels at different positions."
)
return pin # same position, no ambiguity

if in_labels:
if self.pinlabels.count(pin) > 1:
raise Exception(
f'Pin label "{pin}" in connector "{self.name}" '
f"is defined more than once."
)
return self.pins[self.pinlabels.index(pin)]

if in_pins:
return pin

raise Exception(
f'Unknown pin "{pin}" for connector "{self.name}"!'
)

def activate_pin(self, pin: Pin, side: Optional[Side]) -> None:
self.visible_pins[pin] = True
if side == Side.LEFT:
self.ports_left = True
Expand Down Expand Up @@ -347,6 +413,7 @@ def __post_init__(self) -> None:
self.wirecount = len(self.colors)

if self.wirelabels:
self.wirelabels = [normalize_pin(w) for w in self.wirelabels]
if self.shield and "s" in self.wirelabels:
raise Exception(
'"s" may not be used as a wire label for a shielded cable.'
Expand Down
40 changes: 17 additions & 23 deletions src/wireviz/Harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,28 +105,17 @@ def connect(
to_name: str,
to_pin: (int, str),
) -> None:
# check from and to connectors
for name, pin in zip([from_name, to_name], [from_pin, to_pin]):
# resolve pin labels to pin numbers via Connector.resolve_pin()
for name, pin, is_from in [
(from_name, from_pin, True),
(to_name, to_pin, False),
]:
if name is not None and name in self.connectors:
connector = self.connectors[name]
# check if provided name is ambiguous
if pin in connector.pins and pin in connector.pinlabels:
if connector.pins.index(pin) != connector.pinlabels.index(pin):
raise Exception(
f"{name}:{pin} is defined both in pinlabels and pins, for different pins."
)
# TODO: Maybe issue a warning if present in both lists but referencing the same pin?
if pin in connector.pinlabels:
if connector.pinlabels.count(pin) > 1:
raise Exception(f"{name}:{pin} is defined more than once.")
index = connector.pinlabels.index(pin)
pin = connector.pins[index] # map pin name to pin number
if name == from_name:
from_pin = pin
if name == to_name:
to_pin = pin
if not pin in connector.pins:
raise Exception(f"{name}:{pin} not found.")
resolved = self.connectors[name].resolve_pin(pin)
if is_from:
from_pin = resolved
else:
to_pin = resolved

# check via cable
if via_name in self.cables:
Expand Down Expand Up @@ -280,9 +269,14 @@ def create_graph(self) -> Graph:
else:
raise Exception("No side for loops")
for loop in connector.loops:
# Convert pin numbers to 1-based port indices.
# Ports are named p{index+1} in the connector table,
# not p{pin_number} — these differ for non-sequential pins.
idx0 = connector.pins.index(loop[0]) + 1
idx1 = connector.pins.index(loop[1]) + 1
dot.edge(
f"{connector.name}:p{loop[0]}{loop_side}:{loop_dir}",
f"{connector.name}:p{loop[1]}{loop_side}:{loop_dir}",
f"{connector.name}:p{idx0}{loop_side}:{loop_dir}",
f"{connector.name}:p{idx1}{loop_side}:{loop_dir}",
label=" ", # Work-around to avoid over-sized loops.
)

Expand Down
17 changes: 12 additions & 5 deletions src/wireviz/wv_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ def mm2_equiv(awg):
return mm2_equiv_table.get(str(awg), "Unknown")


def normalize_pin(value):
"""Normalize a pin value: try int() first, fall back to str().

Matches the coercion convention used by expand().
"""
try:
return int(value)
except (ValueError, TypeError):
return str(value) if value is not None else value


def expand(yaml_data):
# yaml_data can be:
# - a singleton (normally str or int)
Expand Down Expand Up @@ -61,11 +72,7 @@ def expand(yaml_data):
# '-' was not a delimiter between two ints, pass e through unchanged
output.append(e)
else:
try:
x = int(e) # single int
except Exception:
x = e # string
output.append(x)
output.append(normalize_pin(e))
return output


Expand Down
Loading