mohan-bee/drone

This code defines the physical hardware layout and component placements for a drone's circuit board, including outlines, cutouts, and detailed PCB footprints for various electronic components like microcontrollers, sensors, connectors, and passive devices.

Version
1.0.11
License
unset
Stars
0

work/extract_kicad_netlist.py

from __future__ import annotations

import json
import math
import sys
from collections import defaultdict
from pathlib import Path

import sexpdata

SOURCE = Path("/Users/mohan/Downloads/ESP32 drone.kicad_sch")


def tag(item):
    return str(item[0]) if isinstance(item, list) and item else ""


def children(item, name):
    return [x for x in item if tag(x) == name]


def child(item, name):
    return next((x for x in item if tag(x) == name), None)


def prop(item, name, default=""):
    p = next(
        (
            x
            for x in item
            if tag(x) == "property" and len(x) >= 3 and x[1] == name
        ),
        None,
    )
    return p[2] if p else default


def point(item):
    return (round(float(item[1]), 3), round(float(item[2]), 3))


class UnionFind:
    def __init__(self):
        self.parent = {}

    def add(self, x):
        self.parent.setdefault(x, x)

    def find(self, x):
        self.add(x)
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]
            x = self.parent[x]
        return x

    def union(self, a, b):
        a, b = self.find(a), self.find(b)
        if a != b:
            self.parent[b] = a


data = sexpdata.load(SOURCE.open())
lib_symbols_node = child(data, "lib_symbols")
lib_symbols = {
    item[1]: item
    for item in lib_symbols_node[1:]
    if tag(item) == "symbol" and len(item) > 1
}


def pins_in_lib_symbol(node):
    found = {}

    def walk(item):
        if not isinstance(item, list):
            return
        if tag(item) == "pin":
            at = child(item, "at")
            number = child(item, "number")
            name = child(item, "name")
            if at and number:
                found[str(number[1])] = {
                    "name": str(name[1]) if name else "",
                    "at": (float(at[1]), float(at[2])),
                }
            return
        for sub in item:
            walk(sub)

    walk(node)
    return found


library_pins = {name: pins_in_lib_symbol(node) for name, node in lib_symbols.items()}
instances = [x for x in data if tag(x) == "symbol"]
wires = [x for x in data if tag(x) == "wire"]
labels = [x for x in data if tag(x) in ("label", "global_label")]
junctions = [point(child(x, "at")) for x in data if tag(x) == "junction"]

uf = UnionFind()
segments = []
for wire in wires:
    pts = child(wire, "pts")
    ends = children(pts, "xy")
    if len(ends) >= 2:
        a, b = point(ends[0]), point(ends[-1])
        uf.union(a, b)
        segments.append((a, b))


def on_segment(p, a, b, tolerance=0.002):
    cross = (p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0])
    if abs(cross) > tolerance:
        return False
    return (
        min(a[0], b[0]) - tolerance <= p[0] <= max(a[0], b[0]) + tolerance
        and min(a[1], b[1]) - tolerance <= p[1] <= max(a[1], b[1]) + tolerance
    )


attachment_points = set(junctions)
for label in labels:
    attachment_points.add(point(child(label, "at")))

component_pins = []
power_pins = []
component_inventory = []

for instance in instances:
    lib_id = str(child(instance, "lib_id")[1])
    ref = str(prop(instance, "Reference"))
    value = str(prop(instance, "Value"))
    footprint = str(prop(instance, "Footprint"))
    at = child(instance, "at")
    ix, iy = float(at[1]), float(at[2])
    rotation = float(at[3]) if len(at) > 3 else 0
    mirror_node = child(instance, "mirror")
    mirror = str(mirror_node[1]) if mirror_node else ""
    rad = math.radians(rotation)
    instance_pin_numbers = {str(p[1]) for p in children(instance, "pin")}
    pin_defs = library_pins.get(lib_id, {})
    is_power = lib_id.startswith("power:")

    if not is_power:
        component_inventory.append(
            {
                "reference": ref,
                "value": value,
                "footprint": footprint,
                "lib_id": lib_id,
            }
        )

    for number in instance_pin_numbers:
        definition = pin_defs.get(number)
        if not definition:
            continue
        x, y = definition["at"]
        if mirror == "x":
            y = -y
        elif mirror == "y":
            x = -x
        gx = ix + math.cos(rad) * x + math.sin(rad) * y
        gy = iy + math.sin(rad) * x - math.cos(rad) * y
        coord = (round(gx, 3), round(gy, 3))
        attachment_points.add(coord)
        record = {
            "reference": ref,
            "value": value,
            "pin": number,
            "pin_name": definition["name"],
            "coord": coord,
        }
        if is_power:
            power_pins.append(record)
        else:
            component_pins.append(record)

# Join every explicit attachment point to each segment it lies on.
for p in attachment_points:
    uf.add(p)
    for a, b in segments:
        if on_segment(p, a, b):
            uf.union(p, a)

# Labels with the same name are electrically common on this single sheet.
label_coords = defaultdict(list)
for label in labels:
    label_coords[str(label[1])].append(point(child(label, "at")))
for coords in label_coords.values():
    for coord in coords[1:]:
        uf.union(coords[0], coord)

# KiCad power symbols of the same value are global.
power_coords = defaultdict(list)
for pin in power_pins:
    power_coords[pin["value"]].append(pin["coord"])
for coords in power_coords.values():
    for coord in coords[1:]:
        uf.union(coords[0], coord)

names_by_root = defaultdict(set)
for name, coords in label_coords.items():
    for coord in coords:
        names_by_root[uf.find(coord)].add(name)
for name, coords in power_coords.items():
    for coord in coords:
        names_by_root[uf.find(coord)].add(name)

pins_by_root = defaultdict(list)
for pin in component_pins:
    root = uf.find(pin["coord"])
    pins_by_root[root].append(pin)

nets = []
for root, pins in pins_by_root.items():
    names = sorted(names_by_root.get(root, []))
    auto_name = "~".join(
        f'{p["reference"]}.{p["pin"]}'
        for p in sorted(pins, key=lambda p: (p["reference"], p["pin"]))
    )
    nets.append(
        {
            "name": "/".join(names) if names else auto_name,
            "labels": names,
            "pins": sorted(pins, key=lambda p: (p["reference"], p["pin"])),
        }
    )

result = {
    "components": sorted(component_inventory, key=lambda c: c["reference"]),
    "nets": sorted(nets, key=lambda n: n["name"]),
}
json.dump(result, sys.stdout, indent=2)