Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

kicad-ipc-rs is a production-ready Rust client for KiCad’s IPC API.

Why this crate?

kicad-ipc-rs gives you programmatic control over KiCad with an ergonomic, type-safe Rust API. Whether you’re building automation tools, integrating KiCad into CI/CD pipelines, or creating custom workflows, this crate provides the most complete and well-documented interface to KiCad’s API.

Key Features

  • 100% API Coverage: All 57 KiCad v10.0.0 API commands implemented
  • Type-Safe Models: Native Rust structs for tracks, vias, footprints, nets, and more
  • Dual API: Async-first design with full synchronous support via blocking feature
  • Zero Protobuf Hassle: Pre-generated types — no KiCad source checkout needed
  • Battle-Tested: Used in real automation and integration workflows

API Comparison

Capabilitykicad-ipc-rsPython bindingsOfficial Rust
Rust-native API✅ Production-ready❌ Python only⚠️ Preview
Async + Sync✅ Both supported⚠️ Event-loop⚠️ Preview
Complete coverage✅ 57/57 commandsUnknownUnknown
Active maintenance✅ Yes✅ Official⚠️ Preview

Project Goals

  • Rust-native API for all KiCad IPC commands
  • Typed, ergonomic models for board and editor operations
  • Full parity between async and blocking APIs
  • Clear documentation and real-world examples
  • Stable, maintainable release workflow

Current Scope

  • KiCad API proto snapshot pinned in repo (src/proto/generated/)
  • 57/57 wrapped command families from KiCad v10.0.0
  • Runtime compatibility verified against KiCad 10.0.0

Core Entrypoints

  • Async: kicad_ipc_rs::KiCadClient
  • Blocking: kicad_ipc_rs::KiCadClientBlocking (enable blocking feature)
  • Errors: kicad_ipc_rs::KiCadError

Getting Started

Jump to Quickstart to connect to KiCad and run your first commands.

Quickstart

Prereqs

  1. KiCad running on the same machine.
  2. IPC socket available (default discovery, or KICAD_API_SOCKET).
  3. Optional auth token in KICAD_API_TOKEN if your setup requires it.

Async API (default)

Cargo.toml:

[dependencies]
kicad-ipc-rs = "0.4.3"
tokio = { version = "1", features = ["macros", "rt"] }
use kicad_ipc_rs::KiCadClient;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::builder()
        .client_name("quickstart-async")
        .connect()
        .await?;

    client.ping().await?;
    let version = client.get_version().await?;
    println!("KiCad: {}", version.full_version);
    Ok(())
}

Blocking API

Cargo.toml:

[dependencies]
kicad-ipc-rs = { version = "0.4.3", features = ["blocking"] }
use kicad_ipc_rs::KiCadClientBlocking;

fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClientBlocking::builder()
        .client_name("quickstart-blocking")
        .connect()?;

    client.ping()?;
    let version = client.get_version()?;
    println!("KiCad: {}", version.full_version);
    Ok(())
}

Environment Variables

VariablePurposeUsed by
KICAD_API_SOCKETExplicit IPC socket URI/path overrideasync + blocking
KICAD_API_TOKENIPC auth tokenasync + blocking

Next Steps

Usage Patterns

This chapter targets repeatable integration patterns for tool builders and code generators.

Pattern: Cheap Health Check

Use at process startup to validate socket + auth + server liveness.

use kicad_ipc_rs::KiCadClient;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;
    client.ping().await?;
    Ok(())
}

Pattern: Read-only Query Pipeline

Recommended order for board-aware reads:

  1. get_open_documents()
  2. get_nets()
  3. get_items_by_net(...) or get_items_by_type_codes(...)

Reason: fail fast on document state before expensive item traversal.

Pattern: Safe Write Session

Use begin/end commit around mutating commands.

  1. begin_commit(...)
  2. create_items(...) / update_items(...) / update_editable_items(...) / delete_items(...)
  3. end_commit(..., CommitAction::Commit, ...)

If errors mid-flight: close with CommitAction::Abort/Drop per flow.

Pattern: Editable Item Mutation

Use EditablePcbItem when you want to round-trip existing board items without manually decoding and packing protobuf Any payloads.

use kicad_ipc_rs::{CommitAction, EditablePcbItem, KiCadClient};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;
    let commit = client.begin_commit().await?;

    let mut items = client.get_editable_items_by_type_codes(vec![
        KiCadClient::pcb_object_type_codes()
            .iter()
            .find(|entry| entry.name == "KOT_PCB_TRACE")
            .expect("trace object type should exist")
            .code,
    ]).await?;

    for item in &mut items {
        if let EditablePcbItem::Track(track) = item {
            track.set_layer_id(0);
        }
    }

    client.update_editable_items(items).await?;
    client
        .end_commit(commit, CommitAction::Commit, "move tracks to layer")
        .await?;

    Ok(())
}

Prefer typed wrapper methods like set_layer_id, set_layer_ids, and position setters. Use proto_mut() only for advanced cases where the typed editable API does not yet expose the field you need.

Common Pitfalls

PitfallSymptomAvoidance
Assume KiCad always runningconnect errors at startupexplicit prereq check + ping()
Skip open-document checkdownstream command failurescall get_open_documents() first
Mix sync + async API unintentionallyduplicate runtime ownershippick one surface per process
Fire write commands without commit sessionpartial or rejected mutationsalways bracket writes with commit APIs
Hardcode unsupported commandsAS_UNHANDLED at runtimemap/handle RunActionStatus and runtime flags
Use read models for mutationno way to write the item back losslesslyfetch EditablePcbItem instead of PcbItem

Async vs Blocking Selection

RequirementPreferred API
Tokio app / async daemonKiCadClient
Existing sync binaryKiCadClientBlocking
Lowest integration friction for scriptsKiCadClientBlocking + CLI

Reliability Checklist

  • Set explicit client_name for traceability.
  • Keep request timeout defaults unless measured need.
  • Handle transport + protocol errors as recoverable boundary.
  • Use typed wrappers when available; drop to raw only when needed.

Examples

Real-world usage patterns for kicad-ipc-rs.

Quick Version Probe (Async)

use kicad_ipc_rs::KiCadClient;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;
    let version = client.get_version().await?;
    println!("{:?}", version);
    Ok(())
}

Open Board Detection (Blocking)

use kicad_ipc_rs::KiCadClientBlocking;

fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClientBlocking::connect()?;
    let has_board = client.has_open_board()?;
    println!("open board: {}", has_board);
    Ok(())
}

Example: Editable Item Mutation

Fetch editable tracks, mutate them in place, and write them back as one undoable KiCad commit:

use kicad_ipc_rs::{CommitAction, EditablePcbItem, KiCadClient};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;

    let trace_type = KiCadClient::pcb_object_type_codes()
        .iter()
        .find(|entry| entry.name == "KOT_PCB_TRACE")
        .expect("trace object type should exist")
        .code;

    let mut items = client
        .get_editable_items_by_type_codes(vec![trace_type])
        .await?;

    for item in &mut items {
        if let EditablePcbItem::Track(track) = item {
            track.set_layer_id(0);
        }
    }

    let commit = client.begin_commit().await?;
    client.update_editable_items(items).await?;
    client
        .end_commit(commit, CommitAction::Commit, "update editable tracks")
        .await?;

    Ok(())
}

Example: PCB Analysis - Find Unconnected Nets

Analyze a board to find nets that aren’t properly connected:

use kicad_ipc_rs::KiCadClient;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;
    
    // Get all nets in the current board
    let nets = client.get_nets().await?;
    
    // Filter for nets with names suggesting they're unconnected
    let suspicious: Vec<_> = nets
        .iter()
        .filter(|net| {
            net.name.to_lowercase().contains("unconnected") ||
            net.name.to_lowercase().contains("unrouted") ||
            net.name.starts_with("Net-(")
        })
        .collect();
    
    if suspicious.is_empty() {
        println!("All nets appear to be properly connected!");
    } else {
        println!("Found {} potentially unconnected nets:", suspicious.len());
        for net in suspicious {
            println!("  - {} (code: {})", net.name, net.code);
        }
    }
    
    Ok(())
}

Example: PCB Analysis - List All Footprints

Get a summary of all footprints on the board:

use kicad_ipc_rs::{KiCadClient, PcbObjectTypeCode};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;
    
    // Get all footprints
    let footprints = client.get_items_by_type_codes(vec![
        PcbObjectTypeCode::new_footprint()
    ]).await?;
    
    let mut by_lib: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    
    for item in footprints {
        if let kicad_ipc_rs::PcbItem::Footprint(fp) = item {
            let lib = fp.library_id.unwrap_or_else(|| "Unknown".to_string());
            *by_lib.entry(lib).or_insert(0) += 1;
        }
    }
    
    println!("Footprints by library:");
    for (lib, count) in by_lib.iter().take(10) {
        println!("  {}: {}", lib, count);
    }
    
    Ok(())
}

Example: Automation - Batch Rename Text Variables

Update text variables across the project:

use kicad_ipc_rs::{KiCadClient, DocumentType};
use std::collections::BTreeMap;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;
    
    // Get current text variables
    let current = client.get_text_variables().await?;
    println!("Current variables: {:?}", current);
    
    // Add/update variables
    let mut updates = current.clone();
    updates.insert("VERSION".to_string(), "v2.1.0".to_string());
    updates.insert("DATE".to_string(), "2026-03-29".to_string());
    
    // Set the updated variables
    client.set_text_variables(updates, 
        kicad_ipc_rs::MapMergeMode::Replace
    ).await?;
    
    println!("Text variables updated successfully");
    Ok(())
}

Example: Automation - Add Test Points to Unconnected Pads

Automatically add test point footprints to pads that aren’t connected to nets:

use kicad_ipc_rs::{KiCadClient, CommitAction, KiCadError, PcbItem};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClient::connect().await?;
    
    // Get all pads and filter for unconnected ones
    let items = client.get_all_pcb_items().await?;
    
    let mut unconnected_pads = Vec::new();
    for item in items {
        if let PcbItem::Pad(pad) = item {
            if pad.net_code.is_none() && pad.pad_number != "1" {
                unconnected_pads.push(pad);
            }
        }
    }
    
    if unconnected_pads.is_empty() {
        println!("No unconnected pads found");
        return Ok(());
    }
    
    println!("Found {} unconnected pads to add test points", unconnected_pads.len());
    
    // Start commit session
    let commit = client.begin_commit().await?;
    
    // For each unconnected pad, add a test point footprint
    // (simplified - actual implementation would create footprint items)
    for pad in unconnected_pads.iter().take(5) {
        println!("Would add test point near pad {} at {:?}", 
            pad.pad_number, pad.position_nm);
    }
    
    // Commit the changes
    client.end_commit(
        commit.id,
        CommitAction::Commit,
        "Added test points to unconnected pads"
    ).await?;
    
    Ok(())
}

Example: CI/CD - Design Rule Check Integration

Script to run automated checks before committing to version control:

use kicad_ipc_rs::KiCadClientBlocking;

fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClientBlocking::connect()?;
    
    // Check 1: Verify board is open
    if !client.has_open_board()? {
        eprintln!("ERROR: No board is open in KiCad");
        std::process::exit(1);
    }
    
    // Check 2: Get all nets and look for DRC markers
    let nets = client.get_nets()?;
    println!("✓ Board has {} nets", nets.len());
    
    // Check 3: Verify board origin is set
    let origin = client.get_board_origin(
        kicad_ipc_rs::BoardOriginKind::Drill
    )?;
    println!("✓ Board origin at ({}, {})", origin.x_nm, origin.y_nm);
    
    // Check 4: Save the board before proceeding
    client.save_document()?;
    println!("✓ Board saved");
    
    // Check 5: Export board as string for diffing
    let board_string = client.get_board_as_string()?;
    println!("✓ Board exported ({} bytes)", board_string.len());
    
    println!("\nAll checks passed! Board is ready for commit.");
    Ok(())
}

Example: Integration - Net Class Validation

Verify that all nets have appropriate net classes assigned:

use kicad_ipc_rs::KiCadClientBlocking;
use std::collections::BTreeSet;

fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClientBlocking::connect()?;
    
    // Get all net classes
    let net_classes = client.get_net_classes()?;
    let class_names: BTreeSet<_> = net_classes
        .iter()
        .map(|nc| nc.name.clone())
        .collect();
    
    // Get all nets
    let nets = client.get_nets()?;
    
    // Check each net has a valid net class
    let mut missing_class = Vec::new();
    let netclass_map = client.get_netclass_for_nets(
        nets.iter().map(|n| n.code).collect()
    )?;
    
    for (net_code, class_entry) in netclass_map {
        if class_entry.net_class_name.is_empty() {
            let net = nets.iter().find(|n| n.code == net_code).unwrap();
            missing_class.push(net.name.clone());
        }
    }
    
    if missing_class.is_empty() {
        println!("✓ All {} nets have net classes assigned", nets.len());
    } else {
        println!("⚠ {} nets without net classes:", missing_class.len());
        for net in missing_class.iter().take(10) {
            println!("  - {}", net);
        }
    }
    
    Ok(())
}

Example: Working with Selections

Programmatically select and modify items:

use kicad_ipc_rs::KiCadClientBlocking;

fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
    let client = KiCadClientBlocking::connect()?;
    
    // Get current selection summary
    let summary = client.get_selection_summary(vec![])?;
    println!("Currently selected: {} items", summary.total_count);
    
    // Clear selection
    let result = client.clear_selection()?;
    println!("Cleared {} items from selection", result.summary.total_count);
    
    // Get all tracks
    let tracks = client.get_items_by_type_codes(vec![
        kicad_ipc_rs::PcbObjectTypeCode::new_trace()
    ])?;
    
    // Select first 5 tracks
    let track_ids: Vec<_> = tracks.iter()
        .take(5)
        .filter_map(|item| {
            if let kicad_ipc_rs::PcbItem::Track(t) = item {
                t.id.clone()
            } else {
                None
            }
        })
        .collect();
    
    if !track_ids.is_empty() {
        let result = client.add_to_selection(track_ids)?;
        println!("Added {} tracks to selection", result.summary.total_count);
    }
    
    Ok(())
}

CLI Testing Tool

A CLI tool is available for rapid command testing and debugging:

cargo run --features blocking --bin kicad-ipc-cli -- help

Common commands:

# Basic connectivity
cargo run --features blocking --bin kicad-ipc-cli -- ping
cargo run --features blocking --bin kicad-ipc-cli -- version

# Board queries
cargo run --features blocking --bin kicad-ipc-cli -- board-open
cargo run --features blocking --bin kicad-ipc-cli -- nets
cargo run --features blocking --bin kicad-ipc-cli -- pcb-types

# Selection
cargo run --features blocking --bin kicad-ipc-cli -- selection-summary
cargo run --features blocking --bin kicad-ipc-cli -- clear-selection

Full command catalog: docs/TEST_CLI.md

Next Steps

Validation and Testing

Before handoff or release:

cargo fmt --all
cargo test
cargo test --features blocking

Evidence Pointers

CI Notes

  • API/release pipeline: .github/workflows/release-plz.yml
  • Book deploy pipeline: .github/workflows/mdbook.yml

API Reference

Primary API docs live on docs.rs:

Key items:

  • KiCadClient (async)
  • KiCadClientBlocking (blocking feature)
  • KiCadError
  • Typed models under model::*

PCB item API layers:

  • Raw IPC: *_raw methods return prost_types::Any payloads for direct protobuf interop.
  • Read model: PcbItem and related Pcb* structs are lightweight decoded models for inspection.
  • Editable model: EditablePcbItem and typed wrappers preserve the full protobuf payload for mutate/update workflows.

Editable item helpers:

  • get_editable_items_by_id(...)
  • get_editable_items_by_type_codes(...)
  • create_editable_items(...)
  • update_editable_items(...)

Use EditablePcbItem when you need to fetch existing board items, mutate fields like layer or position, and write them back through KiCad IPC without hand-building protobuf Any payloads. The editable wrappers expose proto(), proto_mut(), and into_proto() as advanced escape hatches when typed helpers are not enough.

Selection API notes:

  • get_selection_* methods now take type_codes: Vec<i32> (Vec::new() means no filter).
  • add_to_selection, remove_from_selection, clear_selection return SelectionMutationResult (decoded items + summary).
  • get_selection_as_string returns SelectionStringDump (ids + contents).