Files
kicad-zone-resistance/fill_resistance/main.py
T
janik 7604e18587
Build PCM package / build (push) Successful in 6s
Fail usefully on NixOS: probe QtCore, explain the missing-.so error
First NixOS attempt: pip wheel PySide6 cannot load libgthread-2.0.so.0
because NixOS has no FHS library paths. Nothing inside the venv can
fix that (KiCad installs wheels only) - but the plugin made it worse
twice over:

- The backend probe imported bare PySide6, whose pure-Python __init__
  succeeds even when QtCore's .so cannot load - so matplotlib was
  promised QtAgg and the error figure died at switch_backend, taking
  the failure report with it. The probe now imports <binding>.QtCore
  and falls through to Tk/Agg on a broken Qt.
- The user got a raw ImportError traceback. A cannot-open-shared-object
  failure during the kipy/dialog imports now raises a UserFacingError
  that names the actual fixes: run KiCad in an FHS environment
  (steam-run) or enable nix-ld with Qt runtime libraries. The README
  Linux notes carry the same guidance.

141 passed on the dev stack and the Python 3.9 mac-equivalent stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:22:22 +07:00

155 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Top-level orchestration for the KiCad-launched action.
Flow: connect -> read the two selected contacts (rectangles/pads) ->
gather fills -> selection dialog (net, layers, contacts, current, cell)
-> extract vias -> solve -> figures + report.
Every failure is reported twice: on stdout (lands in the KiCad status-bar
warning list) and as a matplotlib error figure, so it cannot be missed.
"""
from __future__ import annotations
import sys
import traceback
from . import config, pipeline, progress, report
from .errors import CandidateError, UserFacingError
def _fail(message: str, outdir) -> None:
print(f"ERROR: {message}")
try:
if outdir is None:
# A failure before the run has an output directory (a broken
# plugin environment throws on import) would otherwise save
# no PNG - and with no GUI toolkit, plots falls back to
# opening the saved PNGs, so the figure would never be shown
# either. Exactly the case the docstring promises to cover.
import tempfile
from pathlib import Path
outdir = Path(tempfile.gettempdir()) / "fill-resistance-error"
from . import plots
fig = plots.fig_error(message)
plots.save_and_show([(fig, "error")], outdir)
except Exception: # reporting must not mask the fault
traceback.print_exc()
sys.exit(1)
def main() -> None:
outdir = None
try:
try:
from kipy.errors import ApiError
from . import board_io, dialog
except ImportError as e:
if "cannot open shared object file" not in str(e):
raise
# pip's Linux wheels link against FHS system libraries;
# on NixOS those paths don't exist and PySide6/pynng die
# exactly like this. Nothing inside the venv can fix it.
raise UserFacingError(
f"A compiled dependency cannot load its system "
f"libraries: {e}\nThe plugin venv is built from pip "
f"wheels, which expect standard (FHS) library paths. "
f"On NixOS, run KiCad inside an FHS environment (e.g. "
f"steam-run) or enable programs.nix-ld with Qt's "
f"runtime libraries - see README, Platform notes."
)
try:
kicad, board = board_io.connect()
stackup = board_io.get_stackup_info(board)
es1, es2, net_hint = board_io.get_electrodes(board, stackup)
if board_io.any_zone_unfilled(board) or config.ALWAYS_REFILL:
board_io.refill(board)
fills = board_io.gather_net_fills(board)
tracks = board_io.gather_net_tracks(board)
copper = board_io.merge_copper(
fills, board_io.tracks_as_polygons(tracks))
candidate_nets = board_io.nets_overlapping(copper, es1, es2)
buildups = board_io.gather_mask_buildups(board)
except ApiError as e:
raise UserFacingError(
f"KiCad API error: {e}\nIf KiCad is showing a dialog, close "
f"it and run again."
)
if not candidate_nets:
raise CandidateError(
"No copper (zone fill or trace) overlaps both contacts. "
"Check that both sit over copper of the same net and that "
"the fills are up to date (press B in the board editor)."
)
def group_label(parts):
names = [p.label for p in parts[:3]]
more = f" +{len(parts) - 3}" if len(parts) > 3 else ""
return f"{len(parts)}× " + ", ".join(names) + more
def group_contact(parts):
contacts = {p.contact for p in parts}
return contacts.pop() if len(contacts) == 1 else "auto"
default_net = (net_hint if net_hint in candidate_nets
else candidate_nets[0])
selection = dialog.ask(
candidates={n: list(copper[n].keys()) for n in candidate_nets},
layer_order=stackup.names,
default_net=default_net,
e1_label=group_label(es1), e2_label=group_label(es2),
contact1=group_contact(es1), contact2=group_contact(es2),
buildup_layers=sorted(buildups.keys()),
)
if selection is None:
print("cancelled")
return
# the solve owns the thread from here; without this the plugin
# looks like it did nothing until the figures appear
progress.start()
if selection.contact1 != "auto":
for e in es1:
e.contact = selection.contact1
if selection.contact2 != "auto":
for e in es2:
e.contact = selection.contact2
if selection.cell_um is not None:
config.CELL_UM_OVERRIDE = selection.cell_um
config.ADAPTIVE_CELLS = selection.adaptive
try:
problem = board_io.build_problem(
board, selection.net, selection.layers, es1, es2, stackup,
fills,
buildups=(buildups if selection.include_buildup else None),
extra_cu_um=selection.extra_cu_um,
tracks=(tracks if selection.include_tracks else None),
vias_capped=selection.vias_capped,
cap_max_drill_mm=selection.cap_max_drill_mm)
outdir = report.make_output_dir(board_io.board_dir(board))
except ApiError as e:
raise UserFacingError(f"KiCad API error: {e}")
report.write_geometry_dump(outdir, problem)
overlay_cb = None
if selection.push_overlays:
def overlay_cb(stack, result):
board_io.push_result_overlays(board, stack, result)
pipeline.run(problem, outdir, show=True, i_test=selection.current_a,
freq_hz=selection.freq_hz,
contact_model=selection.contact_model,
overlay=overlay_cb)
except progress.Cancelled:
print("cancelled") # user's own doing: no error figure
except UserFacingError as e:
_fail(str(e), outdir)
except Exception:
_fail(traceback.format_exc(), outdir)
finally:
progress.done() # also on the error paths
if __name__ == "__main__":
main()