skidl icon indicating copy to clipboard operation
skidl copied to clipboard

Export to Eeschema Schematic

Open johnthagen opened this issue 5 years ago • 19 comments

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.

johnthagen avatar Dec 06 '19 13:12 johnthagen

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.

xesscorp avatar Dec 06 '19 13:12 xesscorp

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?

johnthagen avatar Dec 06 '19 13:12 johnthagen

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.

xesscorp avatar Dec 09 '19 17:12 xesscorp

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.

johnthagen avatar Dec 09 '19 18:12 johnthagen

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 avatar Dec 09 '19 21:12 xesscorp

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

image

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.

johnthagen avatar Dec 21 '19 14:12 johnthagen

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.

xesscorp avatar Dec 21 '19 18:12 xesscorp

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/

johnthagen avatar Jul 15 '20 18:07 johnthagen

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

falkartis avatar Aug 06 '20 11:08 falkartis

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.

johnthagen avatar Aug 06 '20 12:08 johnthagen

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.

falkartis avatar Aug 06 '20 16:08 falkartis

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]

ryanm101 avatar Nov 09 '20 21:11 ryanm101

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.

johnthagen avatar Feb 02 '21 14:02 johnthagen

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

shanemmattner avatar Aug 08 '21 15:08 shanemmattner

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.

devbisme avatar Aug 09 '21 15:08 devbisme

I'm making progress and now have parts being semi-auto placed within subcircuits and the nets from passives to U? parts auto drawn.

Screenshot from 2021-08-12 07-45-24

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

shanemmattner avatar Aug 12 '21 15:08 shanemmattner

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.

devbisme avatar Aug 12 '21 16:08 devbisme

@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

johnthagen avatar Aug 12 '21 17:08 johnthagen

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. auto_layout

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)

shanemmattner avatar Aug 15 '21 00:08 shanemmattner