KiCommand discussion and development - Easy pcbnew command line; 4.07 & 5.1.5/.6

Edit: KiCommand is now released on github
There is still a lot of work to do. Be careful docking the window, and ALWAYS save before executing a command. Crashing and creating bizarre objects are to be expected.


I’m asking everyone so I can understand if there is any interest in a dialog box that accepts easy to construct commands in pcbnew.

I don’t yet know how many types of commands I can implement, but here is what I have so far. Right now it’s just retrieving items (PADS, MODULES, TRACKS) either all or only the selected items or unselected items. Then selecting or unselecting them. Once I add commands such as ‘RELATIVEMOVE’ or ‘MOVE’, and the ability to select on a few more parameters, then things will get more interesting.

    # This allows simple command strings to be executed within pcbnew
    # The command string is in postfix format, and the current commands are:
    # Arguments, if any, are taken from the top of the stack.
    # and results, if any, are placed back on the stack.

    # COPY           - Copy the top of the stack.
    # MODULES        - Put all modules on the stack.
    # PADS           - Put all pads on the stack.
    # TRACKS         - Put all tracks on the stack.
    # SELECTED       - Filter top of the stack for selected items.
    # NOTSELECTED    - Filter top of the stack for unselected items.
    # SETSELECT      - Set items on the top of the stack to selected.
    # CLEARSELECT    - Set items on the top of the stack to unselected.
    # MATCHREFERENCE - Filter list of modules for matching a reference.
    # GETPADS        - Get all pads on the list of modules.

    # command_stack.runcommand(command_string)
    #
    # Sample command_string:
    #
    # 'MODULES' - return the list of modules
    # 'MODULES SELECTED' - return the list of selected modules
    # 'MODULES SELECTED CLEARSELECT' - unselect all selected modules
    # 'MODULES SETSELECT' - select all modules (this seems to have no visual effect)
    # 'PADS SETSELECT' - select all pads
    # 'PADS CLEARSELECT' - unselect all pads
    # 'MODULES GETPADS SETSELECT' - select all pads of all modules
    # 'MODULES GETPADS CLEARSELECT' - unselect all pads of all modules
    # 'MODULES U1 MATCHREFERENCE GETPADS SETSELECT' - select the pads of the module with reference 'U1'
7 Likes

After adding quite a few commands, I can do the following:

command_stack.runcommand(‘CLEAR TRACKS COPY SELECTED CONNECTED SETSELECT’)

This will select all tracks connected (recursively) to the selected track.

  • CLEAR - Clear the operand stack.
  • TRACKS - push full track list onto the stack.
  • COPY - make a copy (of the tracks) on the top of the stack.
  • SELECTED - filter the top of the stack (tracks) and get only those that are selected.
  • CONNECTED - get all tracks that have the same vertices as any of the selected tracks. Do this recursively.
  • SETSELECT - select all the connected tracks.

Here is the list of current commands:

Comparison
    ISINSTANCE
    <
Conversion
    MIL
    MM
    FLOAT
    DICT
    MILS
    STRING
Elements
    PADS
    TOPTEXT
    MODULES
    TRACKS
Numeric
    +
    *
    -
    /
Filter
    FILTER
    LIST
    CONNECTED
    MATCHREFERENCE
    EXTEND
Call
    CALLLIST
    GETSTART
    CALL
    GETEND
Action
    SETSELECT
    CLEARSELECT
Attributes
    SELECTED
    INDEX
    NOTSELECTED
    ATTR
Stack
    SUM
    SWAP
    ZIP2
    COPY
    CLEAR
    APPEND

If you have any questions or comments, just ask!

3 Likes

Example: moving a polygon.

Suppose you start with a polygon on F.SilkS:

Then you move one of the lines of the polygon:

Then select the line you just moved and a line in the rest of the polygon:

Then run this command:
command_stack.runcommand(‘REJOIN’)

I’m still working out the kinks and seeing if I can/should expand this beyond top-level lines. It would be useful within the footprint tool, but I haven’t tackled any scripting for that yet.

4 Likes

Don’t be discouraged by the silence in here, I’m probably speaking for a lot of people who absolutely like what you’re doing, but have nothing useful to contribute and just watch.
:thumbsup:

4 Likes

“Square this polygon” would have been helpful to me.

I mooted the idea of a simpler, more application oriented command language than straight Python for KiCad, and although a lot of people seem to misunderstand the proposal, the consensus appeared to be that Python was preferred What type of language would you prefer for writing KiCad scripts?

However, perhaps by creating something concrete people will get a better idea of what it is and whether it might be of use.

The REJOIN command works really well.

I want to create additional commands that deal with connected shapes, treating them as connected entities. Any suggestions on how to differentiate the commands? I need a one or two letter abbreviation, like CS for “Connected Shape” so I can make commands such as:

  • CSALIGNLEFT

  • CSALIGNRIGHT

  • CSALIGNTOP

  • CSALIGNBOTTOM

  • CSALIGNCENTER (with specified angle allows horizontal, vertical, or anything in between)

  • CSROTATE (somehow specify center of rotation, maybe)

  • CSEQUALATERAL (make all sides equal length) (should I make equal to largest, smallest, or average of all sides?)

  • CSLENGTH (specify length of sides)

  • CSMAKEANGLE (make angle of selected side the specified absolute angle in degrees where 0 is horizontal to the left and positive angle is rotating CCW/Leftward)

  • CSMAKEVERTICAL

  • CSMAKEHORIZONTAL

  • CSDISTRIBUTE (distribute shapes equally using either CENTER or EDGE of shapes, within current extremes or specified spacing, and current or specified ANGLE).

  • MOVE (relative move)

These and some basic geometry extractions for calculations:

  • getting center, left, right, top, bottom, angle (for lines) of objects and shapes

Any suggestions on names or additional capability? (No guaranty I’ll use your suggestion though!)

I appreciate the interest and feedback! I read through the entire thread you pointed to @bobc. It was enlightening.

Obviously, you’ll need to be able to support a scripted analogue clock in its own dedicated layer:

:wink:

2 Likes

Do you find these frequent adverts for pcb-rnd help get new users, or does it generally turn people off?

Tbh, kinda sick about hearing about how wonderful pcb-rnd is.

I think the real value is in the information commons, namely, the shared designs, models, footprints and symbols.

To this end, I will happily continue to produce code that cultivates interoperability between FOSS tools and allows hobbyists to pool their collective efforts.

tbh, I don’t mind which FOSS layout tool hobbyists, makers or radio amateurs or STEM educators use.

Different strokes for different folks - but open formats - to me that’s vital.

Nevertheless, this forum is specifically for KiCad, not Open Source tools in general.

It makes it look like you are desperate to get users.

I agree with this attitude. And it’s good to hear from a sister project every so often.

I now have some basic drawing primitives:

DRAWSEGMENTS can be used to draw any set of connected line segments.
DRAWTEXT arguments are text, position, size, thickness, layer
(I’ll add the arguments to DRAWSEGMENTS for thickness and layer soon.

from command_stack import runcommand as r
r('100,100,150,125,125,150,100,100 MM DRAWSEGMENTS')
r('50,50,75,75 MM DRAWSEGMENTS')
r('DrawTextTest 50,20 MM 5,5 MM 1 MM F.SilkS DRAWTEXT')

I’m working on the fluidity of using strings, string lists, floating point, floating point lists, and conversions thereof. Trying to make the conversions seamless from one type to another as needed.

The comma separated strings such as 100,100,150,125,125,150,100,100 are converted to a list of floating point numbers and then each member of the list converted from mm to native units (nm) by the command MM (there are the equivalent commands MIL and MILS). Then when DRAWSEGMENTS is called, the floating point number list is converted in pairs (x,y) to pcbnew.wxPoints automatically. Note that to draw a closed shape, the first and last points must be identical.

To make entry simpler, especially when adding multiple drawing elements, the new DRAWPARAM command sets width, height, thickness, and layer. These values are retained and used for all drawing until changed by another DRAWPARAM command. This way, DRAWSEGMENTS and DRAWTEXT only need to specify coordinates for position and it makes drawing a lot of items in a row much easier.

It is turning me off.

KiCad’s presentation, layout, and tools make it “feel” to me like a professional tool.

“rat lines” … augyhh. The term I’ve always heard is “rats nest”.

“pins/pads”… oyeee. Symbols have pin numbers, footprints have pads.

I am sorta curious about the “far side” selection. It is grey, so am I going to see the dark side of the moon?

As a “sanity check” I have started to draw the body and pin outline of parts on the Fab layer.

If I could create a “U” shape, with the dimensions of the pin, and array that to the body of the part, that would be pretty dang cool.

I have to admit, I still don’t understand what you are trying to accomplish.

What functionality do you want to extend?
Do you want to create the PCB using only keyboard/script commands?
Do you want a keyboard-centric approach to manipulating existing graphic elements (footprints, tracks, etc.)?
Do you want to add the possibility to manipulate many elements at once?
…?

I wanted an easy way to manipulate objects. Having already programmed KiPadCheck, LayerViewSet, and beginning on KiSelect, I started to see that even some of the basic operations in python are not that easy. And that some people would never learn how to do things in python.

I also noticed I would type the same commands in the Scripting Console to accomplish some basic things like selecting or deselecting all objects (to help debugging my other KiCad tools).

Then I realized that instead of defining some fixed commands in python, I could chain them together if I wrote a simple stack-based processor (the processing started out as 8 lines of python, it’s now 50 lines of python including defining new commands).

Then based on @Sprig’s comment and starting with the REJOIN command, i realized that there could be great application in manipulating shapes in KiCad since KiCad doesn’t really support polygons of any sort, a feature could have used as well.

There is also the possibility of sharing command strings, since they are so much more compact than any equivalent python.

So this started out as a sequence of “I wish I had something that could make that easy” to “hmmm, can I do this in a more general way” to “I wonder if this might be useful to anyone else” to “I think I can add even more useful stuff as commands”.

So not really an original intent, but an exploration of the space of what’s possible to make simple scripts a little more accessible.

An example of the simplicity of using command_stack. Here is how to select all Module References in command_stack (at some point there will be a command line dialog):

MODULES GetReference CALL SETSELECT

Here is the same thing in python:

for module in pcbnew.GetBoard().GetModules():
    module.GetReference().SetSelected()

There’s not a big difference here to an experienced programmer. But to someone who has never programmed, the first is a LOT easier.

Edit: formatting, added example

3 Likes

Please, PLEASE do not be dismayed by the number of commands available. Most of the time, you will only likely need a few of them, and the rest are available to “make simple things easy, and complex things possible”.

This is sort of the status of things. There is certainly more to come, including geometry drawing and manipulation commands.

Here are the current list of commands by category, all commands and help text, and all pre-defined user commands.

Commands By Category

r('ALL HELPCAT HELPALL')
     Action: CLEARSELECT SETSELECT REJOIN
 Attributes: ATTR NOTSELECTED INDEX SELECTED
       Call: GETSTART CALLLIST GETEND CALL
 Comparison: FILTERTYPE ISTYPE < =
 Conversion: MIL LIST MILS DICT STRING SPLIT MM FLOAT
       Draw: DRAWSEGMENTS DRAWPARAMS DRAWTEXT
   Elements: DRAWINGS PADS MODULES TRACKS
     Filter: EXTEND MATCHREFERENCE CONNECTED FILTER
       Help: SEE HELPALL HELP SEEALL HELPCAT
      Layer: ONLAYERS SETLAYER LAYERNUMS
    Numeric: SUM * + - /
Programming: 
      Stack: APPEND SWAP PICK ZIP2 CLEAR COPYTOP POP
     System: TIME

All Commands With Help Text

* (Category: Numeric)
    [OPERAND1 OPERAND2] Return the the floating point OPERAND1 *
    OPERAND2.
+ (Category: Numeric)
    [OPERAND1 OPERAND2] Return the the floating point OPERAND1 +
    OPERAND2.
- (Category: Numeric)
    [OPERAND1 OPERAND2] Return the the floating point OPERAND1 -
    OPERAND2.
/ (Category: Numeric)
    [OPERAND1 OPERAND2] Return the the floating point OPERAND1 /
    OPERAND2.
: (Category: Programming)
    Begin the definition of a new command. This is the only
    command in which arguments occur after the command. Command
    definition ends with the semicolon (;). Run command SEEALL
    for more examples.
< (Category: Comparison)
    [LIST VALUE] Create a LIST of True/False values
    corresponding to whether the values in LIST are less than
    VALUE (for use prior to FILTER)
= (Category: Comparison)
    [LIST VALUE] Create a LIST of True/False values
    corresponding to whether the values in LIST equal to VALUE
    (for use prior to FILTER)
APPEND (Category: Stack)
    [OPERAND1 OPERAND2] Return LIST1 and LIST2 concatenated
    together.
ATTR (Category: Attributes)
    [objects attribute] Get specified python attribute of the
    objects
CALL (Category: Call)
    [LIST FUNCTION] Execute python FUNCTION on each member of
    LIST. Return the list of results in the same order as the
    original LIST.
CALLLIST (Category: Call)
    [LIST FUNCTION] Execute python FUNCTION on each member of
    LIST.The FUNCTION must return a list of items (this is
    suitablefor MODULE FUNCTIONs such as GraphicalItems and
    Pads.
CLEAR (Category: Stack)
    Clear the stack.
CLEARSELECT (Category: Action)
    [objects] Deselect the objects
CONNECTED (Category: Filter)
    [WHOLE INITIAL] From objects in WHOLE, select those that are
    connected to objects in iNITIAL (recursevely)
COPYTOP (Category: Stack)
    Duplicate the top object on the stack.
DICT (Category: Conversion)
    [KEYS VALUES] Create a dictionary from KEYS and VALUES
    lists.
DRAWINGS (Category: Elements)
    Get all top-level drawing objects (lines and text)
DRAWPARAMS (Category: Draw)
    [THICKNESS WIDTH HEIGHT LAYER] Set drawing parameters for
    future DRAW commands.
DRAWSEGMENTS (Category: Draw)
    [POINTSLIST] Points list is interpreted as pairs of X/Y
    values. Line segments aredrawn between all successive pairs
    of points, creating a connected sequence of lines.This
    command uses previously set DRAWPARAMS and the points are in
    native units (nm) so using MM or MILS commands is suggested.
DRAWTEXT (Category: Draw)
    [TEXT POSITION] Draws the TEXT at POSITION using previously
    set DRAWPARAMS. Position is in native units (nm) so using MM
    or MILS commands is suggested.
EXTEND (Category: Filter)
    [LIST1 LIST2] Join LIST1 and LIST2
FILTER (Category: Filter)
    [LIST1 TF_LIST] Retain objects in LIST1 where the
    corresponding value in TF_LIST is True, not None, not zero,
    and not zero length
FILTERTYPE (Category: Comparison)
    [LIST TYPE] Retains objects in LIST that are of TYPE
FLOAT (Category: Conversion)
    [OBJECT] Return OBJECT as a floating point value or list.
    OBJECT can be a string, a comma separated list of values, a
    list of strings, or list of numbers.
GETEND (Category: Call)
    [LIST] Get the end wxPoint from the LIST of DRAWSEGMENTS.
GETSTART (Category: Call)
    [LIST] Get the start wxPoint from the LIST of DRAWSEGMENTS.
HELP (Category: Help)
    [COMMAND] Shows help for COMMAND. Precede the COMMAND by
    single quote mark (') so that it doesn't execute.
HELPALL (Category: Help)
    [COMMAND] Shows help for all commands.
HELPCAT (Category: Help)
    [CATEGORY] Shows commands in CATEGORY. CATEGORY value of ALL
    shows all categories.
INDEX (Category: Attributes)
    [objects index] Select an item in the list of objects
ISTYPE (Category: Comparison)
    [LIST TYPE] Create a LIST of True/False values corresponding
    to whether the values in LIST are of TYPE (for use prior to
    FILTER)
LAYERNUMS (Category: Layer)
    [STRING] Get the layer numbers for each layer in comma
    separated STRING.
LIST (Category: Conversion)
    [OBJECT] Make OBJECT into a list (with only OBJECT in it).
MATCHREFERENCE (Category: Filter)
    [MODULES REFERENCE] Filter the MODULES and retain only those
    that match REFERENCE
MIL (Category: Conversion)
    [OBJECT] Return OBJECT as a floating point value or list
    converted from mils to native units (nm). OBJECT can be a
    string, a comma separated list of values, a list of strings,
    or list of numbers.
MILS (Category: Conversion)
    [OBJECT] Return OBJECT as a floating point value or list
    converted from mils to native units (nm). OBJECT can be a
    string, a comma separated list of values, a list of strings,
    or list of numbers.
MM (Category: Conversion)
    [OBJECT] Return OBJECT as a floating point value or list
    converted from mm to native units (nm). OBJECT can be a
    string, a comma separated list of values, a list of strings,
    or list of numbers.
MODULES (Category: Elements)
    Get all modules
NOTSELECTED (Category: Attributes)
    [objects] Get unselected objects
ONLAYERS (Category: Layer)
    [LIST LAYERS] Retains the objects in LIST that exist on any
    of the LAYERS.
PADS (Category: Elements)
    Get all pads
PICK (Category: Stack)
    [NUMBER] Copy the value that is NUMBER of objects deep in
    the stack to the top of the stack.
        Examples:
        0 PICK - copies the top of the stack.
        1 PICK - pushes a copy of the second item from the
    top of the stack onto the top of the stack.
POP (Category: Stack)
    Removes the top item on the stack.
REJOIN (Category: Action)
    Using selected lines, move multiple connected lines to the
    isolated line.
SEE (Category: Help)
    [COMMAND] Shows previously-defined COMMAND from the user
    dictionary. See the colon (:) command for more information.
SEEALL (Category: Help)
    [COMMAND] Shows all previously-defined COMMANDs from the
    user dictionary. See the colon (:) command for more
    information.
SELECTED (Category: Attributes)
    [objects] Get selected objects
SETLAYER (Category: Layer)
    [OBJECTS LAYER] Moves all OBJECTS to LAYER.
SETSELECT (Category: Action)
    [objects] Select the objects
SPLIT (Category: Conversion)
    [STRING] Split STRING on commas into a list of strings
STRING (Category: Conversion)
    [OBJECT] Convert OBJECT to a string.
SUM (Category: Numeric)
    [LIST] Return the sum of all members in LIST.
SWAP (Category: Stack)
    Switches the two top objects on the stack.
TIME (Category: System)
    Returns the current system time as a string.
TRACKS (Category: Elements)
    Get all tracks (including vias)
ZIP2 (Category: Stack)
    [LIST1 LIST2] Creates a list with parallel objects in LIST1
    and LIST2 together at the same index.
[]

All Pre-Defined User Commands

r('SEEALL')
: ALLMREFERENCE MODULES GetReference CALL ;
: ALLMTEXT MODULES GraphicalItems CALLLIST EDA_TEXT ISINSTANCE GetShownText CALL ;
: ALLMVALUE MODULES GetValue CALL ;
: CLEARALL 
       MODULES COPY GetReference CALL CLEARSELECT
       COPY GetValue CALL CLEARSELECT
       COPY GraphicalItems CALLLIST CLEARSELECT
       CLEARSELECT
       PADS CLEARSELECT
       TRACKS CLEARSELECT
       DRAWINGS CLEARSELECT 
        ;
: CLEARSELECT ClearSelected CALL ;
: COPY 0 PICK ;
: NOT ' = ;
: SETSELECT SetSelected CALL ;
: TOPTEXT DRAWINGS EDA_TEXT ISINSTANCE ;

Am I being too wordy?

I wanted to present the ability to call python object functions with arguments:

# Move all module's Value text to Dwgs.User layer
r('MODULES Value CALL Dwgs.User LAYERNUMS LIST SetLayer CALLARGS')

# Move only selected module's Value text to Dwgs.User layer
r('MODULES SELECTED Value CALL Dwgs.User LAYERNUMS LIST SetLayer CALLARGS')

These could be saved as a function called ASSIGNVALUELAYER like this:

# Usage: 'Dwgs.User ASSIGNVALUELAYER'
# changes selected module's value object to Dwgs.User layer.
r(': ASSIGNVALUELAYER MODULES SELECTED Value CALL SWAP LAYERNUMS LIST SetLayer CALLARGS ;')

And then used like this:

r('Eco1.User ASSIGNVALUELAYER')

Here’s the help text for CALLARGS:

CALLARGS (Category: Call)
    [OBJECTLIST ARGLISTOFLISTS FUNCTION] Execute python FUNCTION
    on each member of OBJECTLIST with arguments in
    ARGLISTOFLISTS. ARGLISTOFLISTS can be shorter than
    OBJECTLIST, in which case ARGLISTOFLISTS elements will be
    repeated (or truncated) to match the length of OBJECTLIST.
    Returns the list of results in the same order as the
    original OBJECTLIST. The commands LIST and ZIP2 will be
    helpful here.