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
blockingfeature - Zero Protobuf Hassle: Pre-generated types — no KiCad source checkout needed
- Battle-Tested: Used in real automation and integration workflows
API Comparison
| Capability | kicad-ipc-rs | Python bindings | Official Rust |
|---|---|---|---|
| Rust-native API | ✅ Production-ready | ❌ Python only | ⚠️ Preview |
| Async + Sync | ✅ Both supported | ⚠️ Event-loop | ⚠️ Preview |
| Complete coverage | ✅ 57/57 commands | Unknown | Unknown |
| 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(enableblockingfeature) - Errors:
kicad_ipc_rs::KiCadError
Getting Started
Jump to Quickstart to connect to KiCad and run your first commands.
Related Docs
- Crate README
- API Reference on docs.rs
- Examples for real-world patterns
Quickstart
Prereqs
- KiCad running on the same machine.
- IPC socket available (default discovery, or
KICAD_API_SOCKET). - Optional auth token in
KICAD_API_TOKENif 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
| Variable | Purpose | Used by |
|---|---|---|
KICAD_API_SOCKET | Explicit IPC socket URI/path override | async + blocking |
KICAD_API_TOKEN | IPC auth token | async + blocking |
Next Steps
- Use
kicad-ipc-clifor rapid command checks. - Follow Validation and Testing before CI/release.
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:
get_open_documents()get_nets()get_items_by_net(...)orget_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.
begin_commit(...)create_items(...)/update_items(...)/update_editable_items(...)/delete_items(...)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
| Pitfall | Symptom | Avoidance |
|---|---|---|
| Assume KiCad always running | connect errors at startup | explicit prereq check + ping() |
| Skip open-document check | downstream command failures | call get_open_documents() first |
| Mix sync + async API unintentionally | duplicate runtime ownership | pick one surface per process |
| Fire write commands without commit session | partial or rejected mutations | always bracket writes with commit APIs |
| Hardcode unsupported commands | AS_UNHANDLED at runtime | map/handle RunActionStatus and runtime flags |
| Use read models for mutation | no way to write the item back losslessly | fetch EditablePcbItem instead of PcbItem |
Async vs Blocking Selection
| Requirement | Preferred API |
|---|---|
| Tokio app / async daemon | KiCadClient |
| Existing sync binary | KiCadClientBlocking |
| Lowest integration friction for scripts | KiCadClientBlocking + CLI |
Reliability Checklist
- Set explicit
client_namefor 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
- Learn about usage patterns for integration best practices
- Check the quickstart for getting connected
- Browse the API reference for complete method documentation
Validation and Testing
Before handoff or release:
cargo fmt --all
cargo test
cargo test --features blocking
Evidence Pointers
- Unit tests across client/model/blocking/CLI parser paths:
- Runtime command coverage matrix:
- README coverage section
- Runtime CLI verification flow:
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(blockingfeature)KiCadError- Typed models under
model::*
PCB item API layers:
- Raw IPC:
*_rawmethods returnprost_types::Anypayloads for direct protobuf interop. - Read model:
PcbItemand relatedPcb*structs are lightweight decoded models for inspection. - Editable model:
EditablePcbItemand 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 taketype_codes: Vec<i32>(Vec::new()means no filter).add_to_selection,remove_from_selection,clear_selectionreturnSelectionMutationResult(decoded items + summary).get_selection_as_stringreturnsSelectionStringDump(ids+contents).