summaryrefslogtreecommitdiff
path: root/gerbonara/cad/kicad
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-07-18 21:15:08 +0200
committerjaseg <git@jaseg.de>2023-07-18 21:15:08 +0200
commit58142cb0c7c0a38dd07592632a7e0c1622cc99d9 (patch)
tree01e6ab907049291391b3fda20412e8e7ac75a791 /gerbonara/cad/kicad
parent08c4091e57d59b6a08cb0e4f4d684ec7967019fc (diff)
downloadgerbonara-58142cb0c7c0a38dd07592632a7e0c1622cc99d9.tar.gz
gerbonara-58142cb0c7c0a38dd07592632a7e0c1622cc99d9.tar.bz2
gerbonara-58142cb0c7c0a38dd07592632a7e0c1622cc99d9.zip
kicad: Add schematic file format support
Diffstat (limited to 'gerbonara/cad/kicad')
-rw-r--r--gerbonara/cad/kicad/base_types.py12
-rw-r--r--gerbonara/cad/kicad/graphical_primitives.py12
-rw-r--r--gerbonara/cad/kicad/pcb.py26
-rw-r--r--gerbonara/cad/kicad/schematic.py293
-rw-r--r--gerbonara/cad/kicad/symbols.py2
5 files changed, 321 insertions, 24 deletions
diff --git a/gerbonara/cad/kicad/base_types.py b/gerbonara/cad/kicad/base_types.py
index fc2df71..f763585 100644
--- a/gerbonara/cad/kicad/base_types.py
+++ b/gerbonara/cad/kicad/base_types.py
@@ -226,6 +226,7 @@ class TextEffect:
hide: Flag() = False
justify: OmitDefault(Justify) = field(default_factory=Justify)
+
@sexp_type('tstamp')
class Timestamp:
value: str = field(default_factory=uuid.uuid4)
@@ -242,6 +243,7 @@ class Timestamp:
def bump(self):
self.value = uuid.uuid4()
+
@sexp_type('uuid')
class UUID:
value: str = field(default_factory=uuid.uuid4)
@@ -258,6 +260,7 @@ class UUID:
def bump(self):
self.value = uuid.uuid4()
+
@sexp_type('tedit')
class EditTime:
value: str = field(default_factory=time.time)
@@ -274,6 +277,15 @@ class EditTime:
def bump(self):
self.value = time.time()
+
+@sexp_type('paper')
+class PageSettings:
+ page_format: str = 'A4'
+ width: float = None
+ height: float = None
+ portrait: Flag() = False
+
+
@sexp_type('property')
class Property:
key: str = ''
diff --git a/gerbonara/cad/kicad/graphical_primitives.py b/gerbonara/cad/kicad/graphical_primitives.py
index ce02df4..9ddd807 100644
--- a/gerbonara/cad/kicad/graphical_primitives.py
+++ b/gerbonara/cad/kicad/graphical_primitives.py
@@ -306,6 +306,18 @@ class DimensionStyle:
keep_text_aligned: Flag() = False
+@sexp_type('image')
+class Image:
+ at: AtPos = field(default_factory=AtPos)
+ scale: Named(float) = None
+ layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
+ data: str = ''
+
+ def offset(self, x=0, y=0):
+ self.at = self.at.with_offset(x, y)
+
+
@sexp_type('dimension')
class Dimension:
locked: Flag() = False
diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py
index aee0d54..2ad126e 100644
--- a/gerbonara/cad/kicad/pcb.py
+++ b/gerbonara/cad/kicad/pcb.py
@@ -59,14 +59,6 @@ class GeneralSection:
thickness: Named(float) = 1.60
-@sexp_type('paper')
-class PageSettings:
- page_format: str = 'A4'
- width: float = None
- height: float = None
- portrait: Flag() = False
-
-
@sexp_type('layers')
class LayerSettings:
index: int = 0
@@ -158,18 +150,6 @@ class Net:
name: str = ''
-@sexp_type('image')
-class Image:
- at: AtPos = field(default_factory=AtPos)
- scale: Named(float) = None
- layer: Named(str) = None
- uuid: UUID = field(default_factory=UUID)
- data: str = ''
-
- def offset(self, x=0, y=0):
- self.at = self.at.with_offset(x, y)
-
-
@sexp_type('segment')
class TrackSegment:
start: Rename(XYCoord) = field(default_factory=XYCoord)
@@ -322,7 +302,7 @@ class Board:
polygons: List(gr.Polygon) = field(default_factory=list)
curves: List(gr.Curve) = field(default_factory=list)
dimensions: List(gr.Dimension) = field(default_factory=list)
- images: List(Image) = field(default_factory=list)
+ images: List(gr.Image) = field(default_factory=list)
# Tracks
track_segments: List(TrackSegment) = field(default_factory=list)
track_arcs: List(TrackArc) = field(default_factory=list)
@@ -368,7 +348,7 @@ class Board:
self.curves.remove(obj)
case gr.Dimension():
self.dimensions.remove(obj)
- case Image():
+ case gr.Image():
self.images.remove(obj)
case TrackSegment():
self.track_segments.remove(obj)
@@ -405,7 +385,7 @@ class Board:
self.curves.append(obj)
case gr.Dimension():
self.dimensions.append(obj)
- case Image():
+ case gr.Image():
self.images.append(obj)
case TrackSegment():
self.track_segments.append(obj)
diff --git a/gerbonara/cad/kicad/schematic.py b/gerbonara/cad/kicad/schematic.py
new file mode 100644
index 0000000..5c73c88
--- /dev/null
+++ b/gerbonara/cad/kicad/schematic.py
@@ -0,0 +1,293 @@
+"""
+Library for handling KiCad's schematic files (`*.kicad_sch`).
+"""
+
+import math
+from pathlib import Path
+from dataclasses import field, KW_ONLY
+from itertools import chain
+import re
+import fnmatch
+import os.path
+
+from .sexp import *
+from .base_types import *
+from .primitives import *
+from .symbols import Symbol
+from . import graphical_primitives as gr
+
+from .. import primitives as cad_pr
+
+from ... import graphic_primitives as gp
+from ... import graphic_objects as go
+from ... import apertures as ap
+from ...layers import LayerStack
+from ...newstroke import Newstroke
+from ...utils import MM, rotate_point
+
+
+@sexp_type('path')
+class SheetPath:
+ path: str = '/'
+ page: Named(str) = '1'
+
+
+@sexp_type('junction')
+class Junction:
+ at: AtPos = field(default_factory=AtPos)
+ diameter: Named(float) = 0
+ color: Color = field(default_factory=lambda: Color(0, 0, 0, 0))
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('no_connect')
+class NoConnect:
+ at: AtPos = field(default_factory=AtPos)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('bus_entry')
+class BusEntry:
+ at: AtPos = field(default_factory=AtPos)
+ size: Rename(XYCoord) = field(default_factory=lambda: XYCoord(2.54, 2.54))
+ stroke: Stroke = field(default_factory=Stroke)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('wire')
+class Wire:
+ points: PointList = field(default_factory=PointList)
+ stroke: Stroke = field(default_factory=Stroke)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('bus')
+class Bus:
+ points: PointList = field(default_factory=PointList)
+ stroke: Stroke = field(default_factory=Stroke)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('polyline')
+class Polyline:
+ points: PointList = field(default_factory=PointList)
+ stroke: Stroke = field(default_factory=Stroke)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('text')
+class Text:
+ text: str = ''
+ exclude_from_sim: Named(YesNoAtom()) = True
+ at: AtPos = field(default_factory=AtPos)
+ effects: TextEffect = field(default_factory=TextEffect)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('label')
+class LocalLabel:
+ text: str = ''
+ at: AtPos = field(default_factory=AtPos)
+ fields_autoplaced: Wrap(Flag()) = False
+ effects: TextEffect = field(default_factory=TextEffect)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('global_label')
+class GlobalLabel:
+ text: str = ''
+ shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive)) = Atom.input
+ at: AtPos = field(default_factory=AtPos)
+ fields_autoplaced: Wrap(Flag()) = False
+ effects: TextEffect = field(default_factory=TextEffect)
+ uuid: UUID = field(default_factory=UUID)
+ properties: List(Property) = field(default_factory=list)
+
+
+@sexp_type('hierarchical_label')
+class HierarchicalLabel:
+ text: str = ''
+ shape: Named(AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive)) = Atom.input
+ at: AtPos = field(default_factory=AtPos)
+ fields_autoplaced: Wrap(Flag()) = False
+ effects: TextEffect = field(default_factory=TextEffect)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('pin')
+class Pin:
+ name: str = '1'
+ uuid: UUID = field(default_factory=UUID)
+
+
+# Suddenly, we're doing syntax like this is yaml or something.
+@sexp_type('path')
+class SymbolCrosslinkSheet:
+ path: str = ''
+ reference: Named(str) = ''
+ unit: Named(int) = 1
+
+
+@sexp_type('project')
+class SymbolCrosslinkProject:
+ project_name: str = ''
+ instances: List(SymbolCrosslinkSheet) = field(default_factory=list)
+
+
+@sexp_type('mirror')
+class MirrorFlags:
+ x: Flag() = False
+ y: Flag() = False
+
+
+@sexp_type('property')
+class DrawnProperty:
+ key: str = None
+ value: str = None
+ at: AtPos = field(default_factory=AtPos)
+ hide: Flag() = False
+ effects: TextEffect = field(default_factory=TextEffect)
+
+
+@sexp_type('symbol')
+class SymbolInstance:
+ name: str = None
+ lib_name: Named(str) = ''
+ lib_id: Named(str) = ''
+ at: AtPos = field(default_factory=AtPos)
+ mirror: OmitDefault(MirrorFlags) = field(default_factory=MirrorFlags)
+ unit: Named(int) = 1
+ in_bom: Named(YesNoAtom()) = True
+ on_board: Named(YesNoAtom()) = True
+ dnp: Named(YesNoAtom()) = True
+ fields_autoplaced: Wrap(Flag()) = True
+ uuid: UUID = field(default_factory=UUID)
+ properties: List(DrawnProperty) = field(default_factory=list)
+ # AFAICT this property is completely redundant.
+ pins: List(Pin) = field(default_factory=list)
+ # AFAICT this property, too, is completely redundant. It ultimately just lists paths and references of at most
+ # three other uses of the same symbol in this schematic.
+ instances: Named(List(SymbolCrosslinkProject)) = field(default_factory=list)
+
+
+@sexp_type('path')
+class SubsheetCrosslinkSheet:
+ path: str = ''
+ page: Named(str) = ''
+
+
+@sexp_type('project')
+class SubsheetCrosslinkProject:
+ project_name: str = ''
+ instances: List(SymbolCrosslinkSheet) = field(default_factory=list)
+
+
+@sexp_type('pin')
+class SubsheetPin:
+ name: str = '1'
+ shape: AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive) = Atom.input
+ at: AtPos = field(default_factory=AtPos)
+ effects: TextEffect = field(default_factory=TextEffect)
+ uuid: UUID = field(default_factory=UUID)
+
+
+@sexp_type('sheet')
+class Subsheet:
+ at: AtPos = field(default_factory=AtPos)
+ size: Rename(XYCoord) = field(default_factory=lambda: XYCoord(2.54, 2.54))
+ fields_autoplaced: Wrap(Flag()) = True
+ stroke: Stroke = field(default_factory=Stroke)
+ fill: gr.FillMode = field(default_factory=gr.FillMode)
+ uuid: UUID = field(default_factory=UUID)
+ _properties: List(DrawnProperty) = field(default_factory=list)
+ pins: List(SubsheetPin) = field(default_factory=list)
+ # AFAICT this is completely redundant, just like the one in SymbolInstance
+ instances: Named(List(SubsheetCrosslinkProject)) = field(default_factory=list)
+ _: KW_ONLY
+ sheet_name: object = field(default_factory=lambda: DrawnProperty('Sheetname', ''))
+ file_name: object = field(default_factory=lambda: DrawnProperty('Sheetfile', ''))
+ parent: object = None
+
+ def __after_parse__(self, parent):
+ self.sheet_name, self.file_name = self._properties
+ self.parent = parent
+
+ def __before_sexp__(self):
+ self._properties = [self.sheet_name, self.file_name]
+
+ def open(self, search_dir=None, safe=True):
+ if search_dir is None:
+ if not self.parent.original_filename:
+ raise FileNotFoundError('No search path given and path of parent schematic unknown')
+ else:
+ search_dir = Path(self.parent.original_filename).parent
+ else:
+ search_dir = Path(search_dir)
+
+ resolved = search_dir / self.file_name.value
+ if safe and os.path.commonprefix((search_dir.parts, resolved.parts)) != search_dir.parts:
+ raise ValueError('Subsheet path traversal to parent directory attempted in Subsheet.open(..., safe=True)')
+
+ return Schematic.open(resolved)
+
+
+SUPPORTED_FILE_FORMAT_VERSIONS = [20220914]
+@sexp_type('kicad_sch')
+class Schematic:
+ _version: Named(int, name='version') = 20211014
+ generator: Named(Atom) = Atom.gerbonara
+ uuid: UUID = field(default_factory=UUID)
+ page_settings: PageSettings = field(default_factory=PageSettings)
+ path: SheetPath = field(default_factory=SheetPath)
+ lib_symbols: Named(Array(Symbol)) = field(default_factory=list)
+ junctions: List(Junction) = field(default_factory=list)
+ no_connects: List(NoConnect) = field(default_factory=list)
+ bus_entries: List(BusEntry) = field(default_factory=list)
+ wires: List(Wire) = field(default_factory=list)
+ buses: List(Bus) = field(default_factory=list)
+ images: List(gr.Image) = field(default_factory=list)
+ polylines: List(Polyline) = field(default_factory=list)
+ texts: List(Text) = field(default_factory=list)
+ local_labels: List(LocalLabel) = field(default_factory=list)
+ global_labels: List(GlobalLabel) = field(default_factory=list)
+ hierarchical_labels: List(HierarchicalLabel) = field(default_factory=list)
+ symbols: List(SymbolInstance) = field(default_factory=list)
+ subsheets: List(Subsheet) = field(default_factory=list)
+ sheet_instances: Named(List(SubsheetCrosslinkSheet)) = field(default_factory=list)
+ _ : SEXP_END = None
+ original_filename: str = None
+
+ @property
+ def version(self):
+ return self._version
+
+ @version.setter
+ def version(self, value):
+ if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
+ raise FormatError(f'File format version {value} is not supported. Supported versions are {", ".join(map(str, SUPPORTED_FILE_FORMAT_VERSIONS))}.')
+
+ def write(self, filename=None):
+ with open(filename or self.original_filename, 'w') as f:
+ f.write(self.serialize())
+
+ def serialize(self):
+ return build_sexp(sexp(type(self), self)[0])
+
+ @classmethod
+ def open(kls, pcb_file, *args, **kwargs):
+ return kls.load(Path(pcb_file).read_text(), *args, **kwargs, original_filename=pcb_file)
+
+ @classmethod
+ def load(kls, data, *args, **kwargs):
+ return kls.parse(data, *args, **kwargs)
+
+
+if __name__ == '__main__':
+ import sys
+ from ...layers import LayerStack
+ sch = Schematic.open(sys.argv[1])
+ print('Loaded schematic with', len(sch.wires), 'wires and', len(sch.symbols), 'symbols.')
+ for subsh in sch.subsheets:
+ subsh = subsh.open()
+ print('Loaded sub-sheet with', len(subsh.wires), 'wires and', len(subsh.symbols), 'symbols.')
+
diff --git a/gerbonara/cad/kicad/symbols.py b/gerbonara/cad/kicad/symbols.py
index de1d23d..7e16d38 100644
--- a/gerbonara/cad/kicad/symbols.py
+++ b/gerbonara/cad/kicad/symbols.py
@@ -414,7 +414,7 @@ SUPPORTED_FILE_FORMAT_VERSIONS = [20211014, 20220914]
@sexp_type('kicad_symbol_lib')
class Library:
_version: Named(int, name='version') = 20211014
- generator: Named(Atom) = Atom.kicad_library_utils
+ generator: Named(Atom) = Atom.gerbonara
symbols: List(Symbol) = field(default_factory=list)
_ : SEXP_END = None
original_filename: str = None