kicad: Add schematic file format support

autoroute
jaseg 2023-07-18 21:15:08 +02:00
rodzic 08c4091e57
commit 58142cb0c7
5 zmienionych plików z 321 dodań i 24 usunięć

Wyświetl plik

@ -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 = ''

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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.')

Wyświetl plik

@ -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