Skip to content
This repository has been archived by the owner on Dec 10, 2024. It is now read-only.

Exports: KiCAD schematic roundtrip #64

Merged
merged 2 commits into from
Sep 16, 2024
Merged
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
10 changes: 9 additions & 1 deletion src/faebryk/libs/kicad/fileformats_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from enum import auto
from typing import Optional

from faebryk.libs.sexp.dataclass_sexp import SymEnum, sexp_field
from faebryk.libs.sexp.dataclass_sexp import Symbol, SymEnum, netlist_type, sexp_field
from faebryk.libs.util import KeyErrorAmbiguous

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -105,6 +105,14 @@ class E_justify(SymEnum):

font: C_font

@staticmethod
def preprocess_shitty_hide(c_effects: netlist_type):
if isinstance(c_effects, list) and c_effects[-1] == Symbol("hide"):
c_effects[-1] = [Symbol("hide"), Symbol("yes")]
return c_effects

hide: bool = False

# Legal:
# (justify mirror right)
# (justify bottom)
Expand Down
98 changes: 63 additions & 35 deletions src/faebryk/libs/kicad/fileformats_sch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from dataclasses import dataclass, field
from enum import StrEnum, auto
from enum import auto
from typing import Optional

from faebryk.libs.kicad.fileformats_common import (
Expand All @@ -9,7 +9,6 @@
C_pts,
C_xy,
C_xyr,
gen_uuid,
)
from faebryk.libs.sexp.dataclass_sexp import SEXP_File, SymEnum, sexp_field

Expand All @@ -22,9 +21,11 @@
class C_property:
name: str = field(**sexp_field(positional=True))
value: str = field(**sexp_field(positional=True))
at: Optional[C_xyr] = None
effects: Optional[C_effects] = None
id: Optional[int] = None
at: Optional[C_xyr] = None
effects: Optional[C_effects] = field(
**sexp_field(preprocessor=C_effects.preprocess_shitty_hide), default=None
)


@dataclass(kw_only=True) # TODO: when to use kw_only?
Expand Down Expand Up @@ -53,7 +54,6 @@ class C_circle:
end: C_xy
stroke: C_stroke
fill: C_fill
uuid: UUID = field(default_factory=gen_uuid)


@dataclass(kw_only=True)
Expand All @@ -63,7 +63,6 @@ class C_arc:
end: C_xy
stroke: C_stroke
fill: C_fill
uuid: UUID = field(default_factory=gen_uuid)


@dataclass(kw_only=True)
Expand All @@ -72,7 +71,6 @@ class C_rect:
end: C_xy
stroke: C_stroke
fill: C_fill
uuid: UUID = field(default_factory=gen_uuid)


@dataclass(kw_only=True)
Expand Down Expand Up @@ -109,21 +107,32 @@ class C_pin_names:
class C_symbol:
@dataclass
class C_pin:
class E_type(StrEnum):
class E_type(SymEnum):
# sorted alphabetically
bidirectional = "bidirectional"
free = "free"
input = "input"
no_connect = "no_connect"
open_collector = "open_collector"
open_emitter = "open_emitter"
output = "output"
passive = "passive"
power_in = "power_in"
power_out = "power_out"
bidirectional = "bidirectional"

class E_style(StrEnum):
line = "line"
tri_state = "tri_state"
unspecified = "unspecified"

class E_style(SymEnum):
# sorted alphabetically
clock = "clock"
clock_low = "clock_low"
edge_clock_high = "edge_clock_high"
input_low = "input_low"
inverted = "inverted"
# Unvalidated
# arrow = "arrow"
# dot = "dot"
# none = "none"
inverted_clock = "inverted_clock"
line = "line"
non_logic = "non_logic"
output_low = "output_low"

@dataclass
class C_name:
Expand Down Expand Up @@ -159,20 +168,20 @@ class C_number:
**sexp_field(multidict=True), default_factory=list
)

class E_show_hide(SymEnum):
class E_hide(SymEnum):
hide = "hide"
show = "show"

@dataclass
class C_power:
pass

name: str = field(**sexp_field(positional=True))
power: Optional[C_power] = None
propertys: list[C_property] = field(
**sexp_field(multidict=True), default_factory=list
propertys: dict[str, C_property] = field(
**sexp_field(multidict=True, key=lambda x: x.name),
default_factory=dict,
)
pin_numbers: E_show_hide = field(default=E_show_hide.show)
pin_numbers: Optional[E_hide] = None
pin_names: Optional[C_pin_names] = None
in_bom: Optional[bool] = None
on_board: Optional[bool] = None
Expand All @@ -199,8 +208,9 @@ class C_pin:
in_bom: bool
on_board: bool
fields_autoplaced: bool = True
propertys: list[C_property] = field(
**sexp_field(multidict=True), default_factory=list
propertys: dict[str, C_property] = field(
**sexp_field(multidict=True, key=lambda x: x.name),
default_factory=dict,
)
pins: list[C_pin] = field(
**sexp_field(multidict=True), default_factory=list
Expand All @@ -222,48 +232,66 @@ class C_wire:

@dataclass
class C_text:
at: C_xy
at: C_xyr
effects: C_effects
uuid: UUID
text: str = field(**sexp_field(positional=True))

@dataclass
class C_sheet:
@dataclass
class C_fill:
color: Optional[str] = None

@dataclass
class C_pin:
class E_type(SymEnum):
# sorted alphabetically
bidirectional = "bidirectional"
input = "input"
output = "output"
passive = "passive"
tri_state = "tri_state"

at: C_xyr
effects: C_effects
uuid: UUID
name: str = field(**sexp_field(positional=True))
type: str = field(**sexp_field(positional=True))
type: E_type = field(**sexp_field(positional=True))

at: C_xy
size: C_xy
stroke: C_stroke
fill: C_fill
uuid: UUID
fields_autoplaced: bool = True
propertys: list[C_property] = field(
**sexp_field(multidict=True), default_factory=list
propertys: dict[str, C_property] = field(
**sexp_field(multidict=True, key=lambda x: x.name),
default_factory=dict,
)
pins: list[C_pin] = field(
**sexp_field(multidict=True), default_factory=list
)

@dataclass
class C_global_label:
shape: str
class E_shape(SymEnum):
# sorted alphabetically
input = "input"
output = "output"
bidirectional = "bidirectional"
tri_state = "tri_state"
passive = "passive"
dot = "dot"
round = "round"
diamond = "diamond"
rectangle = "rectangle"

shape: E_shape
at: C_xyr
effects: C_effects
uuid: UUID
text: str = field(**sexp_field(positional=True))
fields_autoplaced: bool = True
propertys: list[C_property] = field(
**sexp_field(multidict=True), default_factory=list
propertys: dict[str, C_property] = field(
**sexp_field(multidict=True, key=lambda x: x.name),
default_factory=dict,
)

# TODO: inheritance
Expand Down Expand Up @@ -293,7 +321,7 @@ class C_bus_entry:
stroke: C_stroke
uuid: UUID

version: str
version: int = field(**sexp_field(assert_value=20211123))
generator: str
uuid: UUID
paper: str
Expand Down
23 changes: 16 additions & 7 deletions src/faebryk/libs/sexp/dataclass_sexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ class sexp_field(dict[str, Any]):
:param Any assert_value: Assert that the value is equal to this value
:param int order: Order of the field in the sexp, lower is first,
can be less than 0. Only used if not positional.
:param Callable[[Any], Any] | None preprocessor: Run before conversion
"""

positional: bool = False
multidict: bool = False
key: Callable[[Any], Any] | None = None
assert_value: Any | None = None
order: int = 0
preprocessor: Callable[[Any], Any] | None = None

def __post_init__(self):
super().__init__({"metadata": {"sexp": self}})
Expand Down Expand Up @@ -77,6 +79,7 @@ def _convert(
t,
stack: list[tuple[str, type]] | None = None,
name: str | None = None,
sp: sexp_field | None = None,
):
if name is None:
name = "<" + t.__name__ + ">"
Expand All @@ -85,6 +88,10 @@ def _convert(
substack = stack + [(name, t)]

try:
# Run preprocessor, if it exists
if sp and sp.preprocessor:
val = sp.preprocessor(val)

# Recurse (GenericAlias e.g list[])
if (origin := get_origin(t)) is not None:
args = get_args(t)
Expand Down Expand Up @@ -129,7 +136,7 @@ def _convert(
if val == []:
return None

assert False, f"Invalid value for bool: {val}"
raise ValueError(f"Invalid value for bool: {val}")

if isinstance(val, Symbol):
return t(str(val))
Expand Down Expand Up @@ -181,6 +188,7 @@ def _decode[T](
if str(key) + "s" in key_fields or str(key) in key_fields:
ungrouped_key_values.append(val)
continue

unprocessed_indices.add(i)

key_values = groupby(
Expand Down Expand Up @@ -234,15 +242,15 @@ def _decode[T](
if origin is list:
val_t = args[0]
value_dict[name] = [
_convert(_val[1:], val_t, stack, name) for _val in values
_convert(_val[1:], val_t, stack, name, sp) for _val in values
]
elif origin is dict:
if not sp.key:
raise ValueError(f"Key function required for multidict: {f.name}")
key_t = args[0]
val_t = args[1]
converted_values = [
_convert(_val[1:], val_t, stack, name) for _val in values
_convert(_val[1:], val_t, stack, name, sp) for _val in values
]
values_with_key = [(sp.key(_val), _val) for _val in converted_values]

Expand All @@ -260,17 +268,18 @@ def _decode[T](
)
else:
assert len(values) == 1, f"Duplicate key: {name}"
out = _convert(values[0][1:], f.type, stack, name)
out = _convert(values[0][1:], f.type, stack, name, sp)
# if val is None, use default
if out is not None:
value_dict[name] = out

# Positional
for f, v in (it := zip_non_locked(positional_fields.values(), pos_values.values())):
sp = sexp_field.from_field(f)
# special case for missing positional empty StrEnum fields
if isinstance(f.type, type) and issubclass(f.type, StrEnum):
if "" in f.type and not isinstance(v, Symbol):
value_dict[f.name] = _convert(Symbol(""), f.type, stack, f.name)
value_dict[f.name] = _convert(Symbol(""), f.type, stack, f.name, sp)
# only advance field iterator
# if no more positional fields, there shouldn't be any more values
if it.next(0) is None:
Expand All @@ -286,9 +295,9 @@ def _decode[T](
while next_val is not None:
vs.append(next_val)
next_val = it.next(1, None)
out = _convert(vs, f.type, stack, f.name)
out = _convert(vs, f.type, stack, f.name, sp)
else:
out = _convert(v, f.type, stack, f.name)
out = _convert(v, f.type, stack, f.name, sp)

value_dict[f.name] = out

Expand Down
Loading