Skip to content

Perfboard Studio v0.13.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 15:55
· 132 commits to main since this release

Added

  • An example that is a real part list: examples/nano-relay. An Arduino Nano switching
    a 12 V relay through a BC547, fed by a 7805, with a push button and an LED -- imported
    from its KiCad netlist exactly as File ▸ Import KiCad Netlist does, with no footprint
    named for it: the catalog's parts by their values, the rest by their KiCad footprints, the
    Nano's and the LED's pins renumbered by name. It is on the README's new Real parts
    pictures, and tools/build_examples.py builds it through the importer, so it is the
    end-to-end check of that path too. The other examples were rebuilt with this release's
    placer; tools/screenshots.py now takes its pictures with a settings store of its own
    rather than the user's, which it used to rearrange.

  • A T on the sheet. A drawn wire went from a pin to a pin and nowhere else, so a net
    reaching four pins was three wires from pin to pin -- nothing like the one run with
    branches off it that a person draws. The wire tool now takes a drawn wire as either click:
    a pin and then a wire, or a wire and then a pin, and the pin branches off it in a T, with
    a dot where it lands and its last run across the wire rather than along it. A T can land
    on a T. The circuit is still doc.nets: a T joins its pin to the net of the wire it lands
    on, exactly as a wire to one of that wire's pins would. It is stored as the wire it is
    plus the far end of the wire it lands on (SheetWire.tap, written only when set, so
    every sheet drawn before is byte for byte what it was), and where it lands is laid back
    onto that wire each time the sheet is drawn -- a symbol drags only the end runs of its
    wires, so a T in the middle of one stays where it was put. Rubbing a wire out takes the
    T's off it with it, in one undo step; a T whose wire is no longer drawn (its net lost an
    end) goes to its pin instead.

  • A KiCad netlist's parts arrive as what they are. The importer kept the nets and threw
    away everything else a netlist says about a component, and guessed each part from its
    reference letter and pin count: a 7805 in a TO-220 ("U1", three pins) became a DIP-8,
    every electrolytic a disc capacitor, and every part came with no value. Now
    parsers.kicad_parts reads, in order: the value against the catalog ("BC547B", "L7805CV",
    "NE555P" -- with the part's pin names and symbol, when the KiCad footprint is the same
    package); the KiCad footprint name, which KiCad writes as the package's measurements
    (resistors and diodes by span and body, electrolytics and discs by can and lead spacing,
    DIPs, headers, IDC, Phoenix and bornier terminals, LEDs, TO-92, TO-220, HC-49); and only
    then the old guess. A surface-mount footprint, a relay, a push button and a module are
    not mapped and say why. The parts are placed beside what they connect to
    (placer.arrange) rather than in a grid in reference order, with their values, as one
    undo step. Over MCP, import_netlist returns suggested_parts and notes, and
    place_missing=true places them.

  • Which leg is which, from the schematic's pin names. A netlist's pin numbers are the
    symbol's, and they are not always the part's: KiCad's LED is pin 1 = cathode where this
    library's is pin 1 = anode, and a generic Q_NPN_EBC given "BC547" as its value is pin 1 =
    emitter where a BC547's pin 1 is its collector. Taken at its numbers, either netlist wires
    a board for a part that does not exist, and no check can see it, because the netlist is
    what every check is measured against. The netlist now carries each pin's name
    (pinfunction, which the parser used to drop); where every named pin matches exactly one
    pin of the real part, the import renumbers it to the part's own numbers and says so, and
    a transistor or diode whose names cannot be settled is reported instead. An LED with no
    names in the netlist is read by KiCad's own numbering of its footprint. A module comes
    through the same way: KiCad numbers an Arduino Nano, a Pico or a D1 mini down one side
    and up the other, the catalog's are numbered across their rows, and every pin is found by
    its name -- KiCad's overbar markup off (~{RESET} is RESET), a Pico's GPIO26_ADC0 read
    as its silkscreen's GP26, and a name on several pins (two GNDs) matched in order when
    every one of them is in the same net, and never otherwise. A part that has no
    catalog entry but three or more named pins -- a 2N5088, an IC -- gets the schematic's
    names printed beside its pins.

  • Labels written on the board. A perfboard has no silkscreen; what a builder writes on
    one with a marker -- "MOTOR 24V" beside the terminal that takes it, "CAN ->" where the
    chain goes -- is now part of the board: Edit ▸ Add Label…, or Add Label Here… on a
    right-click, dragged into place, double-clicked to change, Delete to remove. A label is
    on the top or the solder side, turns by 90°, is printed on the 1:1 sheet and in the
    guide's pictures, and in 3D is drawn by Qt so a Turkish "ALT YÜZ" or an arrow comes out
    whole (VTK's vector text knows ASCII only). Stored as boardNotes, addressed like a
    mounting hole -- a hole and millimetres off it -- and omitted when there are none, so no
    existing board changes by a byte. Nothing derives anything from a label: DRC, LVS and
    the router ignore it. View ▸ Show Labels on the Board hides them; over MCP,
    add_board_label (the 53rd tool) and remove_board_feature. A label stays on the board:
    a drag stops at its edge, and one whose offset would carry it past the substrate -- on
    adding, editing, or narrowing the border under it -- is refused (off-board,
    would-strand-label).

  • A push button as made: sw-tactile-6x6 and sw-tactile-12x12. A 6 x 6 mm button's
    legs stand 6.5 x 4.5 mm apart -- three holes by two on a perfboard, across the gutter on a
    breadboard -- and the registry's sw-tactile is two holes by one, which no such button
    is. Which legs are which matters more: the pair straight across is one strip of metal,
    the pair down one side is what pressing it joins, and sw-tactile calls the pair its
    pins 1 and 2 stand on one node -- so a KiCad switch guessed onto it was a button pressed
    for ever. The new ones are numbered as KiCad's SW_PUSH footprints are (pins 1 and 2 the
    switched pair, 3 and 4 joined inside to them, named A B A B), have KiCad's models on
    their holes, are in the catalog under Switches, and are what a netlist's SW_PUSH_6mm
    and SW_PUSH-12mm, and a guessed SW, now become. sw-tactile is unchanged, for the
    boards that have it.

  • Real parts by name: the catalog. The library is sixty-one packages, and a package is
    not a part -- a TO-92 is a BC547 or a 2N7000 or a 78L05, and which leg is the base is the
    fact the board depends on. perfboard_studio.catalog lists 60 parts a perfboard is
    actually built from -- bipolar and MOSFET transistors, regulators and references, diodes
    and zeners, a PTC, sensors, DIP ICs, push buttons and five dev boards -- each with its
    package, what its datasheet calls every lead, its symbol, its value and its reference
    letter, and
    source saying whose datasheet that is. They head the Parts panel, grouped by kind and
    found by what they are ("p-channel", "esp32"); picking one places it with all of that
    filled in, dragged or clicked, on the board or the sheet. Nothing in the document refers
    back: what lands is an ordinary part, so a board opens the same anywhere. Over MCP,
    list_catalog lists them and place_component / add_part take part="bc547". In
    Turkish, what each part is and what to check on it are in Turkish, and the filter finds
    a part in either language ("regülatör" or "regulator"); the datasheet citations stay as
    written.

  • A module on header pins: mod-<cols>x<rows>-p<c>-r<r>-<W>x<D>x<T>-s<seat>[-o<X>x<Y>].
    A devkit or a breakout is a board of its own standing on its pins, and box- described
    it as a solid block from the board up. The new module-board archetype knows its pins,
    its own board, how high that sits -- 8.5 mm in a female header, 2.5 mm soldered on its
    own pins -- and the height of what is on it. 3D draws the board on its header strips
    with its pins through it; the guide says to solder the headers and plug the module in
    last. The catalog's ESP32-DevKitC V4 is measured from Espressif's own DXF; the
    ESP32-C3-DevKitM-1, Arduino Nano, Raspberry Pi Pico and WEMOS D1 mini from KiCad's
    footprints. Custom Part… has a module family that takes the pin names too.

  • Pin names on the board, and a switch for them. A part's pin names are printed beside
    its pins in 2D and 3D: on a module's own board as its silkscreen is, and on this board
    just outside anything else -- a DIP's down either side, a terminal's behind it rather
    than across its mouth -- on a dark tag, since white ink across white pad rings cannot
    be read. Only names a part declares are printed off a module. View ▸ Show Pin Names
    turns them off for a crowded board being placed or routed.
    The names are laid out for the whole board, not part by part: on the first real board two
    terminals stood side by side and each one's names ran under the other's body. A name now
    stops at whatever is in its way -- a body, a terminal's mouth, a screw head, a label, the
    board's edge, another name. A part with its pins in one row prints them on its other side
    when that is clear; a name short of room is narrowed, down to 60 % of its width; one with
    none is left off, and the part's tooltip lists it. Both views ask
    bodies.lay_out_pin_names, so they print and leave off the same names, and 3D draws each
    one with Qt, as it does a board label, so "GİRİŞ" keeps its dots.

  • A screw terminal whose wires go in from above: screw-terminal-<n>-v. The pluggable
    block on a vertical (180°) header — what is sold as a "dik klemens" — pinned exactly like
    screw-terminal-<n>, so one swaps for the other without moving a pin. It has no side
    mouth, so terminal-entry-blocked and terminal-entry-faces-in say nothing about it and
    the placer does not pull it to an edge: it is the right part for the middle of a board,
    where the first real board had a side-entry terminal facing its neighbour. The header is
    measured from KiCad's Phoenix MSTBVA 2,5/N-G-5,08 model (N × 5.08 + 2.0 mm long, 8.6 mm
    deep, 12 mm tall); the plug is not in KiCad's library, so the mated 12 × 22 mm is an
    estimate and says so. Offered in Custom Part…; the guide says the wires come from above.

  • Screw terminals of every length are drawn as the real part. Only the 2- and 3-way
    blocks had a KiCad model; a 4-way or a 6-way — generated ids no single model could be
    named for — was a flat box with its openings and screws painted on, and on the first
    real board it looked it. A Phoenix MKDS-1,5 block turns out to be a repetition, like a
    header: an end plate with the first way, identical middle ways, a last way. Three slices
    of the 3-way model, placed one at a pin, match KiCad's own 2- to 16-way models colour by
    colour — 38 KB for every length, where the thirteen models would have been 1.5 MB. A
    build without them draws the generated block exactly as before.

  • A board has a name now, and it is not "untitled". Nothing in the application could
    set the document's name: every board the window or the MCP server created was called
    "untitled" for good, and that is the title every build guide, exported schematic and
    project folder went out with. A board nobody has named takes its file's name the first
    time it is saved; File ▸ Rename Board… names it anything else; new_document and
    save_document on the MCP server take a name. The rename is a command
    (document.rename), so it undoes like any other edit — and a name somebody chose is
    never overwritten by the first-save rule.

  • A custom part's body can sit off its pins. box-6x1-p1-r1-16x14.5x7-o0x6 is a
    16 x 14.5 mm breakout whose six pins run along one edge: its body is 6 mm down the rows
    from the pins' centre. Until now box- centred every body on its pins, so a module with
    its header on one edge could only be described as reaching as far past the header on the
    side it does not use as on the side it does — a CAN transceiver board on the first real
    board laid out with this tool had to be written as 16 x 29 mm to be safe. The offset is
    signed, moves the body and the courtyard, turns with the part, and is what DRC, the placer
    and both views measure. Zero is not written, so every existing id names the same part and
    no fixture moves. The custom-part dialog has two fields for it.

  • IDC box headers: idc-2x3 to idc-2x32. A 2xN header in a shroud, numbered exactly
    as hdr-2xN is — a straight ribbon cable with both ends keyed joins pin N to pin N --
    with the key slot in the long wall beside the pin-1 row, drawn in 2D and cut in 3D, because
    the slot is what the cable's red stripe is lined up against. Wurth WR-BHD 61201621621's
    outline: the pin span plus 10.2 mm long, 9.0 mm wide, 9.1 mm tall, a 4.5 mm slot. The
    guide says where the key goes; the placer wants it at an edge like any connector. It is a
    new body archetype, box-header, and a generated id rather than a library part, so the
    61-part registry and its golden are untouched.

  • A part can say what it is and what it calls its leads. Two optional fields on every
    part, placed or not: pinNames (pin number to the datasheet's name for it) and symbol
    (npn, pnp, nmos, pmos, zener, fuse). They are set in the part's properties —
    a symbol list and a table with a row per footprint pin — and by add_part,
    update_part and place_component on the MCP server. What they change:

    • The schematic draws the real symbol. A TO-220 declared pmos with its leads named
      G, D and S is a P-channel MOSFET with its source on top; a TO-92 declared npn with
      B, C and E is a transistor; a DO-35 declared zener has the bent bar; a disc declared
      fuse is a fuse and not a ceramic capacitor. The package numbers are printed on the
      leads, because the symbol says G and the board says pin 1.
    • A module's box reads like its datasheet. A named pin prints its name inside the
      body and its number on the lead outside, and the box widens to fit the longest name —
      an ESP32 devkit's 38 pins read 3V3, EN, IO21 instead of 1 to 38.
    • The guide orients a three-legged part leg by leg — "G (gate) in P1; D (drain) in
      Q1; S (source) in R1" instead of "check the package outline against the board" — and
      its probes name the pin: "Probe J1 pin 1 (24V-L) and R3 pin 1."

    The registry's refusal stands; only the claim has moved. A TO-92 is still a box
    until somebody says otherwise, because BC547 and 2N3904 share the outline and disagree
    about the base. The pinout is a fact about the PART, and the part is now somewhere it
    can be written down by the one person who knows it. A declaration is also not taken on
    trust: a transistor is drawn only when exactly its three leads are named B/C/E or
    G/D/S, and a zener only when something says which lead is the cathode. Otherwise the
    part is drawn as its package draws it, and the sheet's notes say why.

    Both fields are omitted from the file when empty, so every existing board and all
    fifteen golden fixtures serialize to exactly the bytes they did, and
    DOCUMENT_FORMAT_VERSION has still never moved. Copy and paste carry them.

  • DRC says when a part's body hangs past the edge of the board
    (component-overhangs-edge, a warning). component-off-board only ever asked about PIN
    holes, so a TO-220 on row 1 — all three pins in holes, the body standing a millimetre past
    the substrate — was clean, and the placer had put one exactly there on the first real
    board laid out with this tool and called the placement legal. The rule measures the real
    BODY, not the courtyard: the courtyard is padded by half a pitch, and by that measure
    every resistor on the outermost row reaches a full millimetre over the edge. It measures
    against the substrate, printed border included, not the hole grid. A quarter of a
    millimetre is tolerated, because laid on the edge row a DO-41 reaches 0.08 mm past the
    board and a 3 mm LED 0.23 mm; the parts that genuinely hang over clear it by a margin, a
    TO-92 by 0.58 mm and a TO-220 by 1.03. A warning rather than an error, because a part can
    be meant to overhang — a TO-220 reaching a heatsink off the edge is a real layout.

    The placer prices the same predicate, so Optimize Placement no longer leaves a body
    over the edge that DRC then names, and says n part(s) brought back over the board when
    it clears one. Both read one body (footprints.body_extent, which the 2D and 3D views now
    draw from too), one set of edges (geometry.substrate_edges_mm) and one verdict
    (geometry.hangs_over_edge); a test sweeps all 61 footprints at every rotation, mirrored
    and not, against all four edges and holds the two to the same count. A placement with a
    part over the edge is still legal — that word means "breaks no DRC error", and this is a
    warning — but it costs more than any wire a part could save by standing there.

    The rule found one on a shipped example: lpb1-booster's C3, a 6.3 mm electrolytic on
    column A, stands 1.2 mm past the left edge.

  • A wire on a net that declares a current is measured now. DRC's current-capacity
    rule used to look at solder traces only, so a 14 A motor rail carried by a length of
    AWG 24 was a clean board. PLAN.md §5.2 rule 6 always said "the wire's cross-section or
    the solder trace's"; the wire half is here, under the same rule id. A gauge is allowed
    10 A/mm² of copper — the top of the range DRC already quoted for hookup wire in free air,
    and below every figure of the usual chassis-wiring table — and the finding names the
    gauge that would do.

  • wire-too-thick-for-hole, a new warning: a wire whose copper is wider than the
    board's holes. AWG 18 is 1.02 mm and most perfboard is drilled 1.0 mm, so the heavy wire
    a heavy current asks for is exactly the one that will not go through the board. The step
    in the build guide says the same thing where the builder has the wire in hand, and says
    to lap-solder it onto the pad instead.

  • The autorouter writes the gauge onto the wires it lays for a net that declares a
    current
    , so the file says what was planned — and when that net is later declared to
    carry more, current-capacity notices the wire has been outgrown. A net that declares
    nothing is routed exactly as before, with no gauge stored.

    One module, wiregauge.py, answers what a gauge is, what it carries and which one to
    cut, and DRC, the router and the build guide all ask it — so the cut list cannot name a
    wire the design-rule check would reject.

  • A screw terminal knows which way its wires go in. It takes them through one long
    face, and it used to be a box centred on its pins: a terminal with its mouth pressed
    against a capacitor looked exactly like one that could be wired. The face is MEASURED
    from the KiCad Phoenix MKDS mesh the terminals are drawn with — the openings sit on its
    -y face, which is this frame's +y — and every consumer reads it from one place
    (footprints.wire_entry, footprints.entry_corridor):

    • terminal-entry-blocked, a new warning: another part's body stands within 8 mm of
      the mouth, across the terminal's width. One finding per terminal, naming every part in
      the way. A mouth facing into the board over clear space in the MIDDLE of a board is
      deliberately not reported — a cable can cross a board, and nothing here knows where it
      goes.
    • terminal-entry-faces-in, a new warning: a terminal standing ON an edge — its body
      within two holes of it — with its mouth facing away from that edge. A terminal goes to
      an edge because its wires come from outside; facing in from there, every one of them
      doubles back over the board. On the first real board four terminals of six stood on an
      edge facing in, over clear board, and nothing said so. Two holes and not one because one
      of those four sat a hole and 0.3 mm in. A terminal in a corner may face either edge.
    • The placer prices exactly those (terminal, obstacle) pairs, and — as a preference no
      rule holds — how much board lies between each mouth and the edge it faces, which is
      what turns a terminal round. The arrangement puts each terminal's mouth out of the edge
      it is placed on.
    • Choosing between anneals, what DRC warns cannot be built now outranks what it costs
      to route
      , for bodies over the edge, blocked mouths and terminals facing in from an
      edge. The routed cost alone
      kept whatever board routed cheapest: on the first real board laid out with this tool
      the annealer cleared both blocked terminals, that arrangement routed for 812 against
      the original's 726, and the original — two terminals nobody could push a wire into —
      came back as "nothing cheaper to build".
    • The guide says which edge each terminal's wires come in from, and both views draw
      the openings on the entry face — the generated body too, for the four-way and wider
      blocks KiCad ships no mesh for.

    No golden fixture has a screw terminal, so no recorded finding, placement or render
    moves. The project example's "connectors are on the edge" check now measures from the
    part's courtyard, as the placer's own edge term does: measured from the anchor, a
    three-pin header lying across the right-hand edge read as two holes in.

Fixed

  • Four borrowed 3D models stood beside their holes. The TO-92 was KiCad's
    TO-92_Inline, whose legs are 1.27 mm apart, on a footprint whose legs are 2.54 mm apart:
    its middle leg came down between two holes and the case sat a hole to one side. The 3-hole
    disc capacitor's legs stood 1.3 mm beside both its holes, the tactile switch's missed by
    up to 2 mm, and the relay's model (a CUI SR5) had another pinout than relay-spdt
    altogether. The TO-92 is now KiCad's TO-92_Inline_Wide — the same part with its legs bent
    out to the grid; the disc capacitor, the tactile switch and the radial electrolytics have
    their legs bent onto the holes by the converter, as whoever fits them bends them
    (splay in tools/import_kicad_models.py); and the relay is drawn as its generated box,
    which is its own footprint's size, because no KiCad relay has its on-grid pinout.
    tests/test_model_leads.py measures every lead of every shipped mesh against its hole —
    on the old meshes it fails for exactly these eight.

  • Generated screw terminals were drawn with blotches in 3D. A terminal with no borrowed
    model — four ways and up — had its screw heads' tops exactly in the block's top face and
    its wire openings 0.02 mm proud of the front, and the depth buffer cannot tell either from
    coplanar with the view's near plane pulled in: grey and white patches that moved as the
    board turned. Both now stand 0.15 mm proud.

  • perfboard-studio --lang tr board.perf opens the board. The spelling the README
    gives took tr for a file to open and exited with "Cannot open tr"; only --lang=tr
    worked with a file after it. The value after --lang is no longer read as a path.

  • The build guide printed AWG 18 for any current from 5 A up — for 5 A and for 50 A
    alike, because its table stopped there. Past about 8 A it now names the gauge the current
    needs (AWG 16 to AWG 10). Below that nothing it prints has changed.

  • The cut list ignored a gauge the document stored. A wire saved as AWG 18 was printed
    as whatever the net's current suggested, AWG 24 for a net that declared none. It prints
    the stored gauge now, which is also the gauge DRC measures.

  • The 3D view, the guide's step images and Save Project are fast again on a machine
    without a graphics card
    — a virtual machine, a remote desktop, an old laptop. Before
    its first frame, every 3D view filled two lighting tables at sizes meant for a
    photographed environment, and this room is 64 pixels of smooth gradient; the step images
    also used a view per face, and each one filled the tables again. Both tables are now
    sized for the room and the step images come from one view. On the macOS CI runner, which
    draws with Apple's software renderer, one guide's 33 step images went from 820 s to 30 s;
    on Linux's software OpenGL the first frame is 1.4 s instead of 12.7. The picture is the
    same — within 3 levels in 255 on any pixel.

  • A component-side step image after a solder-side one had slightly different lighting
    from the same step drawn on its own, up to 23 levels in 255. Drawing the solder side made
    the component side's view work its lighting out again. One view for both faces cannot.


Windows - PerfboardStudio_..._Setup.exe. It is unsigned, so SmartScreen will
warn on first run: More info -> Run anyway. A signed build is planned.

Linux - perfboard-studio-...-x86_64.AppImage. chmod +x and run it; it needs
glibc 2.35 or newer. If it will not start, the machine has no FUSE 2: install
libfuse2, or run the file with --appimage-extract-and-run, which unpacks it
instead of mounting it.

macOS - perfboard-studio-...-arm64.dmg, Apple silicon. Drag it to Applications,
then run this before opening it:

xattr -dr com.apple.quarantine /Applications/PerfboardStudio.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first launch.
On macOS 15 and later the dialog it raises defaults to Move to Trash, which
deletes the install. Clearing the flag first avoids the dialog entirely. On an
Intel Mac, install from source.

Anywhere with Python 3.12+: pip install -U perfboard-studio, then perfboard-studio.
Same release, no installer and no SmartScreen warning; Qt and VTK are most of a
400 MB download the first time.

Checking your download - SHA256SUMS carries the SHA-256 of all three
files. sha256sum -c SHA256SUMS on Linux, shasum -a 256 -c SHA256SUMS on
macOS, Get-FileHash <file> in PowerShell. Perfboard Studio does this for you when it
fetches an update itself: Help - Check for Updates. It proves the file
arrived intact, not who built it - that second claim needs the code signing
this project has not bought yet.

This is pre-alpha software. Nobody has yet built a real board by following a
generated guide - check every measurement against the part in your hand.