skidl
skidl copied to clipboard
Export to Eeschema Schematic
Would it be possible for skidl
to export into Eeschema?
The use case, is that it would be really powerful to generate a circuit description in Python code using skdl
and then visualize it in Eeschema.
I've done a small amount of work on generating schematics. Once you've got part placement and routing done, then generating a specific format like EESCHEMA wouldn't be hard. The hard part is generating an attractive placement and routing.
The hard part is generating an attractive placement and routing.
Thanks for the response.
This was my thought as well. The thought I had was that if you were able to drop into the schematic directly from Python, either the Python side could try to do a small best-effort on the placement beforehand, or at least now that the data is in a schematic tool, the user has the option to do some manual edits to clean it up.
So the raw data could at least go directly Python -> Eeschema and the clean up could be deferred to the user?
Generating an EESCHEMA schematic would be relatively simple once a decent placement and routing were determined. Determining the placement and routing is the hard part. Giving a poorly-placed schematic in EESCHEMA is going to drive a user crazy. The components can be dragged into position, but straightening-out the wiring is a lot more difficult with the tools currently available.
The main purpose for the schematic is important. If it's just for verification that the SKiDL is doing what it should, then poor placement is not as important. If it's to generate a schematic to hand off to someone else, then it's a bigger problem.
If it's to generate a schematic to hand off to someone else, then it's a bigger problem.
Yeah, that was the use case I was imagining. I was thinking that if the placement and routing wasn't great, the fact it was going into EESCHEMA (which couldn't be edited afterward) rather than a static PDF would still have value.
But I could very well be underestimating how painful it would be to manually clean up such a schematic manually.
Creating an EESCHEMA schematic, even a bad one, would be better than a PDF. At least it's editable. So that's a good idea. But editing it would still be extremely painful with the current EESCHEMA. I doubt anyone would want to do it twice.
@xesscorp I was thinking about this some more, and I think a middle ground solution could actually be really useful.
Instead of trying to auto-route wires, skidl
could use net labels to represent all of the pin connections in a schematic.
Consider this image taken from an online guide:
Using net labels, I think the only API skidl
would need to provide would be for each part:
- Which schematic page it belongs on
- It's x / y location in the schematic
- Possibly orientation (but this could default to the default for the schematic symbol)
I think this is an exciting idea for a couple reasons:
- It greatly reduces the complexity for
skidl
. Leave the layout/placement to the user, who can still use Python to describe the x/y schematic location - The output is in an editable format, so if the user wants to touch it up after ward (adding wires, etc) they can, but they've got all the net information there to help them
- If the user outputs both Eeschema and Pcbnew output into a single project, I think highlight nets should work across Eeschema and Pcbnew, which would be pretty awesome.
- Using the KiCAD libraries,
skidl
should be able to use the schematic symbols already present and associated with footprints though a part, freeing the user from having to do all of those associated lookups.
This would be a way to start. I think that good part placement is of the most importance and this would be a way to start playing with that. Wiring would be relatively easy to route in after that.
You would need to know the bounding box of each part (to avoid overlaps) and the X/Y location of each pin (to know where to attach net labels). For KiCad parts, you'll find the base information in an attribute called draw
from which you can calculate the bounding box and attachment points.
Blog about upcoming new Schematic format in KiCad 6: https://kicad-pcb.org/blog/2020/05/Development-Highlight-New-schematic-and-symbol-library-file-formats-are-now-the-default/
I find this idea very interesting, my use case would probably be to visually check if what I'm writing is correct. I have used in the past tools graphviz/dot to display dependency trees. It's open source project, I don't know if the licence allows to copy part of the positioning algorithm. It surely doesn't produce the best layout, but most of the times good enough to look at. I've used it to display quite large graphs (A3 paper).
What I think would be a really nice interface for skidl
would be to leave the X/Y and routing up to the user and simply provide a convenient way to translate skidl
Net components into Eeschematic symbol positions.
This would allow skidl
to not have to worry about complex automated positioning and allow users to use what ever algorithms they want.
This would allow skidl to not have to worry about complex automated positioning and allow users to use what ever algorithms they want to use.
Also a good idea, this way it can be kept simple, in this case it would be useful to somehow be able to access the geometry of the symbol, like bounding box, relative pin positions and orientations.
I'd love this feature really useful for reverse engineering a pcb.. Though I had an idea that might be nicer as a starting point:
Use YAML (or JSON) to declare the connections and the devices something like below. Then have that generate your stub code or just use it to create the netlist.
The code below is just a PoC and isnt fully fleshed out yet, but the yaml might make it easier when tracing a board as all i have do to is (in notation form) write the connections as text then python can build that into my schema (per the above with global labels would be AWESOME and a netlist for the PCB).
I'm less concerned about the netlist as part of the reversing i may change the pcb layout or devices from through hole to smd etc.
I've not done it here but you could use the fact python passes by ref to instead of adding a str to the connections add the actual object (infinite recursion :) ) rather than having to do another lookup. but as i said above this is just a quick and dirty example of an idea.
any thoughts?
main.py
import yaml
class Component:
def __init__(self, name, comp_type, pins=2, value=None):
self.name = name
self.type = comp_type
self.value = value
self.pin_count = pins
self.pins = self.__initPins__()
def __initPins__(self):
pins = []
i = 0
while i < self.pin_count:
pins.append(Pin(i))
i += 1
return tuple(pins)
class Pin:
def __init__(self, name=None, pin_type=None):
self.name = name
self.type = pin_type
self.connections = []
def populate_pcb(components_yml):
hsh_comp = {}
for comp in components_yml:
k = next(iter(comp))
i = 0
count = 1 if "count" not in comp[k].keys() else comp[k]['count']
pins = 2 if "pins" not in comp[k].keys() else comp[k]['pins']
value = None if "value" not in comp[k].keys() else comp[k]['value']
comp_type = None if "type" not in comp[k].keys() else comp[k]['type']
# See if we already have a <letter> number and ensure we increment
while k+str(i) in hsh_comp.keys():
i += 1
count = count + i
while i < count:
hsh_comp[k + str(i)] = Component(k + str(i), comp_type, pins, value)
i += 1
hsh_comp['GND'] = Component("GND", "POWER", 1, None)
hsh_comp['VCC'] = Component("VCC", "POWER", 1, "5v")
return hsh_comp
def populate_connections(components, connections):
for conn in connections:
k = next(iter(conn))
if '[' in k:
comp_name = k[0:k.index('[')]
pin_num = int(k[k.index('[')+1:k.index(']')])
else:
comp_name = k
pin_num = 0
((components[comp_name]).pins[pin_num]).connections += conn[k]
def main():
with open(r"example.yaml") as f:
pcb = yaml.load(f, Loader=yaml.FullLoader)
f.close()
print(pcb)
components = populate_pcb(pcb["pcb"]["components"])
populate_connections(components,pcb['pcb']['connections'])
if __name__ == "__main__":
# execute only if run as a script
main()
example.yml
pcb:
components:
- U:
type: "16L8-25C"
count: 5
pins: 20
- J:
type: "spst"
pins: 2
- J:
type: "dpst"
pins: 4
- D:
type: "led"
count: 2
- D:
type: "diode"
- U:
type: "unknown"
pins: 14
- M:
type: "ZVP2106A"
pins: 3
- C:
type: "capacitor"
value: "10uf 63v"
- C:
type: "capacitor"
value: "1uf 25v"
- C:
type: "capacitor"
count: 4
- R:
type: "resistor"
value: "1K"
count: 6
- R:
type: "resistor"
value: "4.7K"
- R:
type: "pot"
value: "12.6K"
pins: 3
- U:
type: "555Timer"
pins: 8
- J:
type: "connector"
pins: 86
connections:
- GND:
- J2[1]
- J2[2]
- J2[3]
- J2[4]
- VCC:
- J2[5]
- C2[0]:
- J2[2]
- R0[0]
- R1[0]
- C2[1]:
- J2[5]
- R0[1]:
- J2[54]
- U0[8]
- U0[4]:
- U2[15]
It looks like the up-and-coming KiCad 6 schematic format should be easier to generate now that it's based on S-expressions: https://techexplorations.com/blog/kicad/kicad-6-review-new-and-improved-features/
Perhaps with KiCad 6 this feature becomes easier to implement.
I've gotten a rough generate_schematic()
to successfully build a KiCAD .sch file with all components. The process is:
For each component in default_circuit.parts:
- Get component info from library
- Parse library info to a schematic part and append to a list
I'm doing a lot of string analysis/building so I feel like my process may be improved. @xesscorp I'd love some feedback. Especially on where to put my helper functions that shouldn't be in circuit.py
https://github.com/shanemmattner/skidl/tree/schematic_generation
Thanks for doing this! It looks like a good start.
circuit.py
should contain anything that's generic for creating schematics: stuff like getting component bounding boxes, location of connection points, placement of components, routing from connection point to connection point that avoids the bounding boxes, etc...
tools/kicad.py
should contain KiCad-specific stuff like computing component bounding boxes, finding connection points, inserting components into KiCad schematic files, creating wires, etc.
I probably wouldn't separate these right now while you're still working out how to do this. Once you've figured out the high-level flow, you can define the abstraction/interface for the lower-level operations and extract the KiCad-specific stuff and place it elsewhere.
I'm making progress and now have parts being semi-auto placed within subcircuits and the nets from passives to U? parts auto drawn.
Questions for you @xesscorp :
- Have you done any work on generating the bounding boxes? My idea right now is to make a rectangle based on the maximum pin distances.
- Is there a way to find a part based on the ref name? Right now when I want to manipulate a Part's parameters I'm ranging through all the parts to find it:
for p in range(len(self.parts)):
if p_name == self.parts[p].ref:
# do something with the part...
- I added a method to part.py, but it seems like all your methods in part.py are calling the function indirectly. Should I change to this strategy and can you explain why you chose this way?: Existing method example:
def generate_pinboxes(self, tool=None):
"""
Generate the pinboxes for arranging parts in a schematic.
"""
import skidl
if tool is None:
tool = skidl.get_default_tool()
try:
gen_func = getattr(self, "_gen_pinboxes_{}".format(tool))
except AttributeError:
log_and_raise(
logger,
ValueError,
"Can't generate pinboxes for a component in an unknown ECAD tool format({}).".format(
tool
),
)
return gen_func()
My method:
def gen_part_eeschema(self, c=[0,0]):
time_hex = hex(int(time.time()))[2:]
out=["$Comp\n"]
out.append("L {}:{} {}\n".format(self.lib.filename, self.name, self.ref))
out.append("U 1 1 {}\n".format(time_hex))
out.append("P {} {}\n".format(str(c[0]), str(c[1])))
# Add part symbols. For now we are only adding the designator
n_F0 = 1
for i in range(len(self.draw)):
if re.search("^DrawF0", str(self.draw[i])):
n_F0 = i
break
out.append('F 0 "{}" {} {} {} {} {} {} {}\n'.format(
self.ref,
self.draw[n_F0].orientation,
str(self.draw[n_F0].x + c[0]),
str(self.draw[n_F0].y + c[1]),
self.draw[n_F0].size,
"000",
self.draw[n_F0].halign,
self.draw[n_F0].valign
))
out.append(" 1 {} {}\n".format(str(c[0]), str(c[1])))
out.append(" {} {} {} {}\n".format(1, 0, 0, -1))
out.append("$EndComp\n")
return ("\n" + "".join(out))
Have you done any work on generating the bounding boxes? My idea right now is to make a rectangle based on the maximum pin distances.
Take a look in tools/kicad.py
at the function _gen_svg_comp_
. This function parses a symbol and creates an SVG representation. Part of that process is finding the bounding box and the locations of pins. That may be useful as a guide for your efforts.
The code is messy and there's too much going on at once. If we were doing this right, we would extract the bounding-box and pin location code into separate functions that could be used by my SVG generation code and your schematic generation code.
Is there a way to find a part based on the ref name?
Use the Part.get()
class function. It will search through the parts looking for a string. Something like this would do what you want:
part_ref = "R3"
found_part = Part.get(part_ref)
it seems like all your methods in part.py are calling the function indirectly.
SKiDL is intended to be a generic method of describing circuits that generates tool-specific netlists on the backend. So part.py
, circuit.py
and others have functions that perform the high-level operations but farm off the low-level operations using indirect calls to tool-specific functions.
@shanemmattner Super minor, but just FYI you can have GitHub syntax highlight your code blocks if you write them like:
```python
def fun() -> None:
pass
Example:
def fun() -> None:
pass
Thank you @devbisme. I've made some progress and can now auto place components based on their hierarchies, where each hierarchy is centered around the first part in the circuit.
All parts start at (hierarchical) 0,0 and then get moved based on which parts have non-stub nets connected to the central part. I made a new Part parameter (sch_bb) for the bounding box. I'd appreciate some feedback on the logic and some ideas on how I can successively place all the components in a hierarchy.
One problem I'm trying to solve is which way to "nudge" components when they run into each other's bounding boxes. Right now I use the direction of the other part's connecting pin and then move in that direction 50mil until I'm clear of all circuit parts. This results in weird placement like D1 in the picture. What's happening is that it moves to match pins with R1, then saw a collision with U1 so it moved down (the direction of R1's pin it connects to) until it was clear of U1.
# Range through the hierarchy and nets to find the parts which need to be moved
second_round = [] # list to store parts we don't place on the first pass
for h in hierarchies:
center_part = hierarchies[h][0].ref
for n in routed_nets:
if n.hierarchy == h:
# find the distance between the pins
dx = n.pins[0].x + n.pins[1].x
dy = n.pins[0].y - n.pins[1].y
# determine which part should move.
# The first part in the hierarch is the center
if n.pins[0].ref == center_part:
p = Part.get(n.pins[1].ref)
p.move_part(dx, dy,self.parts, n.pins[0].orientation)
elif n.pins[1].ref == center_part:
p = Part.get(n.pins[0].ref)
p.move_part(dx, dy,self.parts, n.pins[0].orientation)
else:
second_round.append(n)
# Move the part by dx/dy, then check to see if it's colliding with
# any other part. If it is colliding then move the part move towards the
# direction of the pin it was moving towards
def move_part(self, dx, dy, _parts_list, direction):
self.sch_bb[0] += dx
self.sch_bb[1] -= dy
for pt in _parts_list:
if pt.ref == self.ref:
continue
x1min = self.sch_bb[0] - self.sch_bb[2]
x1max = self.sch_bb[0] + self.sch_bb[2]
x2min = pt.sch_bb[0] - pt.sch_bb[2]
x2max = pt.sch_bb[0] + pt.sch_bb[2]
y1min = self.sch_bb[1] - self.sch_bb[3]
y1max = self.sch_bb[1] + self.sch_bb[3]
y2min = pt.sch_bb[1] - pt.sch_bb[3]
y2max = pt.sch_bb[1] + pt.sch_bb[3]
# Logic to tell whether parts collide.
# Note that the movement direction is opposite of what's intuitive ('R' = move left)
# https://stackoverflow.com/questions/20925818/algorithm-to-check-if-two-boxes-overlap
if (x1min <= x2max) and (x2min <= x1max) and (y1min <= y2max) and (y2min <= y1max):
if direction == 'R':
self.move_part(-50, 0, _parts_list, direction)
elif direction == 'L':
self.move_part(50, 0, _parts_list, direction)
elif direction == 'U':
self.move_part(0, -50, _parts_list, direction)
elif direction == 'D':
self.move_part(0, 50, _parts_list, direction)