feat: add pad polygon and padstack presence read APIs

This commit is contained in:
Milind Sharma 2026-02-19 12:51:06 +08:00
parent 59bc0e7838
commit 21b66d5823
5 changed files with 746 additions and 17 deletions

View File

@ -143,6 +143,18 @@ Dump raw payloads for all PCB object classes:
cargo run --bin kicad-ipc-cli -- items-raw-all-pcb --debug
```
Check whether pads/vias have flashed padstack shapes on specific layers:
```bash
cargo run --bin kicad-ipc-cli -- padstack-presence --item-id <uuid> --layer-id 3 --layer-id 34 --debug
```
Get polygonized pad shape(s) on a specific layer:
```bash
cargo run --bin kicad-ipc-cli -- pad-shape-polygon --pad-id <uuid> --layer-id 3 --debug
```
Dump board text (KiCad s-expression):
```bash
@ -184,9 +196,13 @@ cargo run --bin kicad-ipc-cli -- proto-coverage-board-read
Generate full board-read reconstruction markdown report:
```bash
cargo run --bin kicad-ipc-cli -- board-read-report --out docs/BOARD_READ_REPORT.md
cargo run --bin kicad-ipc-cli -- --timeout-ms 60000 board-read-report --out docs/BOARD_READ_REPORT.md
```
Notes:
- Report output is intentionally capped for very large boards to avoid multi-GB files.
- For full raw payloads, use targeted commands such as `items-raw --debug`, `pad-shape-polygon --debug`, and `padstack-presence --debug`.
Get current project path (derived from open PCB docs):
```bash

View File

@ -6,7 +6,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::envelope;
use crate::error::KiCadError;
use crate::model::board::{
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
ArcStartMidEndNm, BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry,
PadShapeAsPolygonEntry, PadstackPresenceEntry, PolyLineNm, PolyLineNodeGeometryNm,
PolygonWithHolesNm, Vector2Nm,
};
use crate::model::common::{
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
@ -37,6 +39,9 @@ const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
const CMD_GET_ITEMS_BY_NET: &str = "kiapi.board.commands.GetItemsByNet";
const CMD_GET_ITEMS_BY_NET_CLASS: &str = "kiapi.board.commands.GetItemsByNetClass";
const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets";
const CMD_GET_PAD_SHAPE_AS_POLYGON: &str = "kiapi.board.commands.GetPadShapeAsPolygon";
const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str =
"kiapi.board.commands.CheckPadstackPresenceOnLayers";
const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems";
const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById";
@ -57,6 +62,8 @@ const RES_GRAPHICS_DEFAULTS_RESPONSE: &str = "kiapi.board.commands.GraphicsDefau
const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
"kiapi.board.commands.BoardEditorAppearanceSettings";
const RES_NETCLASS_FOR_NETS_RESPONSE: &str = "kiapi.board.commands.NetClassForNetsResponse";
const RES_PAD_SHAPE_AS_POLYGON_RESPONSE: &str = "kiapi.board.commands.PadShapeAsPolygonResponse";
const RES_PADSTACK_PRESENCE_RESPONSE: &str = "kiapi.board.commands.PadstackPresenceResponse";
const RES_VECTOR2: &str = "kiapi.common.types.Vector2";
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse";
@ -66,6 +73,8 @@ const RES_TITLE_BLOCK_INFO: &str = "kiapi.common.types.TitleBlockInfo";
const RES_SAVED_DOCUMENT_RESPONSE: &str = "kiapi.common.commands.SavedDocumentResponse";
const RES_SAVED_SELECTION_RESPONSE: &str = "kiapi.common.commands.SavedSelectionResponse";
const PAD_QUERY_CHUNK_SIZE: usize = 256;
const PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbFootprint as i32,
@ -563,6 +572,126 @@ impl KiCadClient {
Ok(format!("{:#?}", payload.classes))
}
pub async fn get_pad_shape_as_polygon(
&self,
pad_ids: Vec<String>,
layer_id: i32,
) -> Result<Vec<PadShapeAsPolygonEntry>, KiCadError> {
if pad_ids.is_empty() {
return Ok(Vec::new());
}
let board = self.current_board_document_proto().await?;
let mut entries = Vec::new();
let layer_name = layer_to_model(layer_id).name;
for chunk in pad_ids.chunks(PAD_QUERY_CHUNK_SIZE) {
let payload = self
.request_pad_shape_as_polygon(&board, chunk, layer_id)
.await?;
if payload.pads.len() != payload.polygons.len() {
return Err(KiCadError::InvalidResponse {
reason: format!(
"GetPadShapeAsPolygon returned mismatched arrays: pads={}, polygons={}",
payload.pads.len(),
payload.polygons.len()
),
});
}
for (pad, polygon) in payload.pads.into_iter().zip(payload.polygons.into_iter()) {
entries.push(PadShapeAsPolygonEntry {
pad_id: pad.value,
layer_id,
layer_name: layer_name.clone(),
polygon: map_polygon_with_holes(polygon)?,
});
}
}
Ok(entries)
}
pub async fn get_pad_shape_as_polygon_debug(
&self,
pad_ids: Vec<String>,
layer_id: i32,
) -> Result<String, KiCadError> {
if pad_ids.is_empty() {
return Ok("PadShapeAsPolygonResponse { pads: [], polygons: [] }".to_string());
}
let board = self.current_board_document_proto().await?;
let mut debug_chunks = Vec::new();
for chunk in pad_ids.chunks(PAD_QUERY_CHUNK_SIZE) {
let payload = self
.request_pad_shape_as_polygon(&board, chunk, layer_id)
.await?;
debug_chunks.push(format!("{:#?}", payload));
}
Ok(debug_chunks.join("\n\n"))
}
pub async fn check_padstack_presence_on_layers(
&self,
item_ids: Vec<String>,
layer_ids: Vec<i32>,
) -> Result<Vec<PadstackPresenceEntry>, KiCadError> {
if item_ids.is_empty() || layer_ids.is_empty() {
return Ok(Vec::new());
}
let board = self.current_board_document_proto().await?;
let mut entries = Vec::new();
for chunk in item_ids.chunks(PAD_QUERY_CHUNK_SIZE) {
let payload = self
.request_padstack_presence_on_layers(&board, chunk, &layer_ids)
.await?;
for row in payload.entries {
let item = row.item.ok_or_else(|| KiCadError::InvalidResponse {
reason: "PadstackPresenceEntry missing item id".to_string(),
})?;
let layer = layer_to_model(row.layer);
let presence = board_commands::PadstackPresence::try_from(row.presence)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", row.presence));
entries.push(PadstackPresenceEntry {
item_id: item.value,
layer_id: row.layer,
layer_name: layer.name,
presence,
});
}
}
Ok(entries)
}
pub async fn check_padstack_presence_on_layers_debug(
&self,
item_ids: Vec<String>,
layer_ids: Vec<i32>,
) -> Result<String, KiCadError> {
if item_ids.is_empty() || layer_ids.is_empty() {
return Ok("PadstackPresenceResponse { entries: [] }".to_string());
}
let board = self.current_board_document_proto().await?;
let mut debug_chunks = Vec::new();
for chunk in item_ids.chunks(PAD_QUERY_CHUNK_SIZE) {
let payload = self
.request_padstack_presence_on_layers(&board, chunk, &layer_ids)
.await?;
debug_chunks.push(format!("{:#?}", payload));
}
Ok(debug_chunks.join("\n\n"))
}
pub async fn get_board_stackup_debug(&self) -> Result<String, KiCadError> {
let command = board_commands::GetBoardStackup {
board: Some(self.current_board_document_proto().await?),
@ -825,6 +954,55 @@ impl KiCadClient {
ensure_item_request_ok(payload.status)?;
Ok(payload.items)
}
async fn request_pad_shape_as_polygon(
&self,
board: &common_types::DocumentSpecifier,
pad_ids: &[String],
layer_id: i32,
) -> Result<board_commands::PadShapeAsPolygonResponse, KiCadError> {
let command = board_commands::GetPadShapeAsPolygon {
board: Some(board.clone()),
pads: pad_ids
.iter()
.cloned()
.map(|value| common_types::Kiid { value })
.collect(),
layer: layer_id,
};
let response = self
.send_command(envelope::pack_any(&command, CMD_GET_PAD_SHAPE_AS_POLYGON))
.await?;
envelope::unpack_any(&response, RES_PAD_SHAPE_AS_POLYGON_RESPONSE)
}
async fn request_padstack_presence_on_layers(
&self,
board: &common_types::DocumentSpecifier,
item_ids: &[String],
layer_ids: &[i32],
) -> Result<board_commands::PadstackPresenceResponse, KiCadError> {
let command = board_commands::CheckPadstackPresenceOnLayers {
board: Some(board.clone()),
items: item_ids
.iter()
.cloned()
.map(|value| common_types::Kiid { value })
.collect(),
layers: layer_ids.to_vec(),
};
let response = self
.send_command(envelope::pack_any(
&command,
CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS,
))
.await?;
envelope::unpack_any(&response, RES_PADSTACK_PRESENCE_RESPONSE)
}
}
fn map_document_specifier(source: common_types::DocumentSpecifier) -> Option<DocumentSpecifier> {
@ -979,6 +1157,66 @@ fn map_hit_test_result(value: i32) -> ItemHitTestResult {
}
}
fn map_polygon_with_holes(
polygon: common_types::PolygonWithHoles,
) -> Result<PolygonWithHolesNm, KiCadError> {
Ok(PolygonWithHolesNm {
outline: polygon.outline.map(map_polyline).transpose()?,
holes: polygon
.holes
.into_iter()
.map(map_polyline)
.collect::<Result<Vec<_>, _>>()?,
})
}
fn map_polyline(line: common_types::PolyLine) -> Result<PolyLineNm, KiCadError> {
Ok(PolyLineNm {
nodes: line
.nodes
.into_iter()
.map(map_polyline_node)
.collect::<Result<Vec<_>, _>>()?,
closed: line.closed,
})
}
fn map_polyline_node(
node: common_types::PolyLineNode,
) -> Result<PolyLineNodeGeometryNm, KiCadError> {
match node.geometry {
Some(common_types::poly_line_node::Geometry::Point(point)) => {
Ok(PolyLineNodeGeometryNm::Point(map_vector2_nm(point)))
}
Some(common_types::poly_line_node::Geometry::Arc(arc)) => {
let start = arc.start.ok_or_else(|| KiCadError::InvalidResponse {
reason: "polyline arc node missing start point".to_string(),
})?;
let mid = arc.mid.ok_or_else(|| KiCadError::InvalidResponse {
reason: "polyline arc node missing mid point".to_string(),
})?;
let end = arc.end.ok_or_else(|| KiCadError::InvalidResponse {
reason: "polyline arc node missing end point".to_string(),
})?;
Ok(PolyLineNodeGeometryNm::Arc(ArcStartMidEndNm {
start: map_vector2_nm(start),
mid: map_vector2_nm(mid),
end: map_vector2_nm(end),
}))
}
None => Err(KiCadError::InvalidResponse {
reason: "polyline node has no geometry".to_string(),
}),
}
}
fn map_vector2_nm(value: common_types::Vector2) -> Vector2Nm {
Vector2Nm {
x_nm: value.x_nm,
y_nm: value.y_nm,
}
}
fn decode_any<T: prost::Message + Default>(
payload: &prost_types::Any,
expected_type_name: &str,
@ -1473,9 +1711,10 @@ fn default_client_name() -> String {
mod tests {
use super::{
any_to_pretty_debug, ensure_item_request_ok, layer_to_model, map_hit_test_result,
map_item_bounding_boxes, model_document_to_proto, normalize_socket_uri,
pad_netlist_from_footprint_items, select_single_board_document, select_single_project_path,
selection_item_detail, summarize_item_details, summarize_selection, PCB_OBJECT_TYPES,
map_item_bounding_boxes, map_polygon_with_holes, model_document_to_proto,
normalize_socket_uri, pad_netlist_from_footprint_items, select_single_board_document,
select_single_project_path, selection_item_detail, summarize_item_details,
summarize_selection, PCB_OBJECT_TYPES,
};
use crate::error::KiCadError;
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
@ -1839,4 +2078,88 @@ mod tests {
assert!(debug.contains("unparsed_any"));
assert!(debug.contains("raw_len=4"));
}
#[test]
fn map_polygon_with_holes_maps_points_and_arcs() {
let polygon = crate::proto::kiapi::common::types::PolygonWithHoles {
outline: Some(crate::proto::kiapi::common::types::PolyLine {
nodes: vec![
crate::proto::kiapi::common::types::PolyLineNode {
geometry: Some(
crate::proto::kiapi::common::types::poly_line_node::Geometry::Point(
crate::proto::kiapi::common::types::Vector2 { x_nm: 10, y_nm: 20 },
),
),
},
crate::proto::kiapi::common::types::PolyLineNode {
geometry: Some(
crate::proto::kiapi::common::types::poly_line_node::Geometry::Arc(
crate::proto::kiapi::common::types::ArcStartMidEnd {
start: Some(crate::proto::kiapi::common::types::Vector2 {
x_nm: 0,
y_nm: 0,
}),
mid: Some(crate::proto::kiapi::common::types::Vector2 {
x_nm: 5,
y_nm: 5,
}),
end: Some(crate::proto::kiapi::common::types::Vector2 {
x_nm: 10,
y_nm: 0,
}),
},
),
),
},
],
closed: true,
}),
holes: vec![crate::proto::kiapi::common::types::PolyLine {
nodes: vec![crate::proto::kiapi::common::types::PolyLineNode {
geometry: Some(
crate::proto::kiapi::common::types::poly_line_node::Geometry::Point(
crate::proto::kiapi::common::types::Vector2 { x_nm: 1, y_nm: 1 },
),
),
}],
closed: true,
}],
};
let mapped = map_polygon_with_holes(polygon).expect("polygon mapping should succeed");
let outline = mapped.outline.expect("outline should be present");
assert_eq!(outline.nodes.len(), 2);
assert!(outline.closed);
assert_eq!(mapped.holes.len(), 1);
}
#[test]
fn map_polygon_with_holes_rejects_missing_arc_points() {
let polygon = crate::proto::kiapi::common::types::PolygonWithHoles {
outline: Some(crate::proto::kiapi::common::types::PolyLine {
nodes: vec![crate::proto::kiapi::common::types::PolyLineNode {
geometry: Some(
crate::proto::kiapi::common::types::poly_line_node::Geometry::Arc(
crate::proto::kiapi::common::types::ArcStartMidEnd {
start: Some(crate::proto::kiapi::common::types::Vector2 {
x_nm: 0,
y_nm: 0,
}),
mid: None,
end: Some(crate::proto::kiapi::common::types::Vector2 {
x_nm: 10,
y_nm: 0,
}),
},
),
),
}],
closed: false,
}),
holes: Vec::new(),
};
let err = map_polygon_with_holes(polygon).expect_err("missing arc point must fail");
assert!(matches!(err, KiCadError::InvalidResponse { .. }));
}
}

View File

@ -21,7 +21,9 @@ pub(crate) mod proto;
pub use crate::client::{ClientBuilder, KiCadClient};
pub use crate::error::KiCadError;
pub use crate::model::board::{
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
ArcStartMidEndNm, BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry,
PadShapeAsPolygonEntry, PadstackPresenceEntry, PolyLineNm, PolyLineNodeGeometryNm,
PolygonWithHolesNm, Vector2Nm,
};
pub use crate::model::common::{
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,

View File

@ -63,6 +63,47 @@ pub struct PadNetEntry {
pub net_name: Option<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ArcStartMidEndNm {
pub start: Vector2Nm,
pub mid: Vector2Nm,
pub end: Vector2Nm,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PolyLineNodeGeometryNm {
Point(Vector2Nm),
Arc(ArcStartMidEndNm),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolyLineNm {
pub nodes: Vec<PolyLineNodeGeometryNm>,
pub closed: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolygonWithHolesNm {
pub outline: Option<PolyLineNm>,
pub holes: Vec<PolyLineNm>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PadShapeAsPolygonEntry {
pub pad_id: String,
pub layer_id: i32,
pub layer_name: String,
pub polygon: PolygonWithHolesNm,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PadstackPresenceEntry {
pub item_id: String,
pub layer_id: i32,
pub layer_name: String,
pub presence: String,
}
#[cfg(test)]
mod tests {
use std::str::FromStr;

View File

@ -1,3 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::PathBuf;
use std::process::ExitCode;
@ -9,6 +10,12 @@ use kicad_ipc::{
Vector2Nm,
};
const REPORT_MAX_PAD_NET_ROWS: usize = 2_000;
const REPORT_MAX_PRESENCE_ROWS: usize = 2_000;
const REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE: usize = 5;
const REPORT_MAX_ITEM_DEBUG_CHARS: usize = 8_000;
const REPORT_MAX_BOARD_SNAPSHOT_CHARS: usize = 750_000;
#[derive(Debug)]
struct CliConfig {
socket: Option<String>,
@ -57,6 +64,16 @@ enum Command {
ItemsRawAllPcb {
include_debug: bool,
},
PadShapePolygon {
pad_ids: Vec<String>,
layer_id: i32,
include_debug: bool,
},
PadstackPresence {
item_ids: Vec<String>,
layer_ids: Vec<i32>,
include_debug: bool,
},
TitleBlock,
BoardAsString,
SelectionAsString,
@ -369,6 +386,70 @@ async fn run() -> Result<(), KiCadError> {
}
}
}
Command::PadShapePolygon {
pad_ids,
layer_id,
include_debug,
} => {
let rows = client
.get_pad_shape_as_polygon(pad_ids.clone(), layer_id)
.await?;
println!(
"pad_shape_total={} layer_id={} requested_pad_count={}",
rows.len(),
layer_id,
pad_ids.len()
);
for row in &rows {
let outline_nodes = row
.polygon
.outline
.as_ref()
.map(|outline| outline.nodes.len())
.unwrap_or(0);
println!(
"pad_id={} layer_id={} layer_name={} outline_nodes={} hole_count={}",
row.pad_id,
row.layer_id,
row.layer_name,
outline_nodes,
row.polygon.holes.len()
);
}
if include_debug {
let debug = client
.get_pad_shape_as_polygon_debug(pad_ids, layer_id)
.await?;
println!("debug={}", debug.replace('\n', "\\n").replace('\t', " "));
}
}
Command::PadstackPresence {
item_ids,
layer_ids,
include_debug,
} => {
let rows = client
.check_padstack_presence_on_layers(item_ids.clone(), layer_ids.clone())
.await?;
println!(
"padstack_presence_total={} requested_item_count={} requested_layer_count={}",
rows.len(),
item_ids.len(),
layer_ids.len()
);
for row in &rows {
println!(
"item_id={} layer_id={} layer_name={} presence={}",
row.item_id, row.layer_id, row.layer_name, row.presence
);
}
if include_debug {
let debug = client
.check_padstack_presence_on_layers_debug(item_ids, layer_ids)
.await?;
println!("debug={}", debug.replace('\n', "\\n").replace('\t', " "));
}
}
Command::TitleBlock => {
let title_block = client.get_title_block_info().await?;
println!("title={}", title_block.title);
@ -648,6 +729,111 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> {
let include_debug = args.iter().any(|arg| arg == "--debug");
Command::ItemsRawAllPcb { include_debug }
}
"pad-shape-polygon" => {
let mut pad_ids = Vec::new();
let mut layer_id = None;
let mut include_debug = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--pad-id" => {
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
reason: "missing value for pad-shape-polygon --pad-id".to_string(),
})?;
pad_ids.push(value.clone());
i += 2;
}
"--layer-id" => {
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
reason: "missing value for pad-shape-polygon --layer-id".to_string(),
})?;
layer_id =
Some(value.parse::<i32>().map_err(|err| KiCadError::Config {
reason: format!(
"invalid pad-shape-polygon --layer-id `{value}`: {err}"
),
})?);
i += 2;
}
"--debug" => {
include_debug = true;
i += 1;
}
_ => {
i += 1;
}
}
}
if pad_ids.is_empty() {
return Err(KiCadError::Config {
reason: "pad-shape-polygon requires one or more `--pad-id <uuid>` arguments"
.to_string(),
});
}
Command::PadShapePolygon {
pad_ids,
layer_id: layer_id.ok_or_else(|| KiCadError::Config {
reason: "pad-shape-polygon requires `--layer-id <i32>`".to_string(),
})?,
include_debug,
}
}
"padstack-presence" => {
let mut item_ids = Vec::new();
let mut layer_ids = Vec::new();
let mut include_debug = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--item-id" => {
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
reason: "missing value for padstack-presence --item-id".to_string(),
})?;
item_ids.push(value.clone());
i += 2;
}
"--layer-id" => {
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
reason: "missing value for padstack-presence --layer-id".to_string(),
})?;
layer_ids.push(value.parse::<i32>().map_err(|err| KiCadError::Config {
reason: format!(
"invalid padstack-presence --layer-id `{value}`: {err}"
),
})?);
i += 2;
}
"--debug" => {
include_debug = true;
i += 1;
}
_ => {
i += 1;
}
}
}
if item_ids.is_empty() {
return Err(KiCadError::Config {
reason: "padstack-presence requires one or more `--item-id <uuid>` arguments"
.to_string(),
});
}
if layer_ids.is_empty() {
return Err(KiCadError::Config {
reason: "padstack-presence requires one or more `--layer-id <i32>` arguments"
.to_string(),
});
}
Command::PadstackPresence {
item_ids,
layer_ids,
include_debug,
}
}
"title-block" => Command::TitleBlock,
"board-as-string" => Command::BoardAsString,
"selection-as-string" => Command::SelectionAsString,
@ -704,13 +890,13 @@ fn default_config() -> CliConfig {
CliConfig {
socket: None,
token: None,
timeout_ms: 3_000,
timeout_ms: 15_000,
}
}
fn print_help() {
println!(
"kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--timeout-ms N] <command> [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type <type>] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id <uuid> ... Show parsed details for specific item IDs\n item-bbox --id <uuid> ... Show bounding boxes for item IDs\n hit-test --id <uuid> --x-nm <x> --y-nm <y> [--tolerance-nm <n>]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id <id> ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup-debug Dump raw stackup response\n graphics-defaults-debug Dump raw graphics defaults response\n appearance-debug Dump raw editor appearance settings response\n netclass-debug Dump raw netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type <t>] Show board origin (`grid` default, or `drill`)\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n"
"kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--timeout-ms N] <command> [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type <type>] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id <uuid> ... Show parsed details for specific item IDs\n item-bbox --id <uuid> ... Show bounding boxes for item IDs\n hit-test --id <uuid> --x-nm <x> --y-nm <y> [--tolerance-nm <n>]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id <id> ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id <uuid> ... --layer-id <i32> [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id <uuid> ... --layer-id <i32> ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup-debug Dump raw stackup response\n graphics-defaults-debug Dump raw graphics defaults response\n appearance-debug Dump raw editor appearance settings response\n netclass-debug Dump raw netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type <t>] Show board origin (`grid` default, or `drill`)\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n"
);
}
@ -756,12 +942,13 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
out.push_str("## Layer / Origin / Nets\n\n");
let enabled = client.get_board_enabled_layers().await?;
let enabled_layers = enabled.layers.clone();
out.push_str(&format!(
"- copper_layer_count: {}\n",
enabled.copper_layer_count
));
out.push_str("- enabled_layers:\n");
for layer in enabled.layers {
for layer in &enabled_layers {
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
}
@ -802,8 +989,15 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
out.push_str("### Pad-Level Netlist (Footprint/Pad/Net)\n\n");
let pad_entries = client.get_pad_netlist().await?;
let mut pad_ids = BTreeSet::new();
out.push_str(&format!("- pad_entry_count: {}\n", pad_entries.len()));
for entry in pad_entries {
for (index, entry) in pad_entries.iter().enumerate() {
if let Some(id) = entry.pad_id.as_ref() {
pad_ids.insert(id.clone());
}
if index >= REPORT_MAX_PAD_NET_ROWS {
continue;
}
out.push_str(&format!(
"- footprint_ref={} footprint_id={} pad_id={} pad_number={} net_code={} net_name={}\n",
entry.footprint_reference.as_deref().unwrap_or("-"),
@ -817,8 +1011,99 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
entry.net_name.as_deref().unwrap_or("-")
));
}
if pad_entries.len() > REPORT_MAX_PAD_NET_ROWS {
out.push_str(&format!(
"- ... omitted {} additional pad net rows (use `netlist-pads` CLI command for full output)\n",
pad_entries.len() - REPORT_MAX_PAD_NET_ROWS
));
}
out.push('\n');
let pad_ids: Vec<String> = pad_ids.into_iter().collect();
let enabled_layer_ids: Vec<i32> = enabled_layers.iter().map(|layer| layer.id).collect();
out.push_str("### Padstack Presence Matrix (Pad IDs x Enabled Layers)\n\n");
out.push_str(&format!(
"- unique_pad_id_count: {}\n- enabled_layer_count: {}\n",
pad_ids.len(),
enabled_layer_ids.len()
));
let mut present_pad_ids_by_layer: BTreeMap<i32, BTreeSet<String>> = BTreeMap::new();
let presence_rows = client
.check_padstack_presence_on_layers(pad_ids.clone(), enabled_layer_ids)
.await?;
out.push_str(&format!(
"- presence_entry_count: {}\n",
presence_rows.len()
));
for row in &presence_rows {
if row.presence == "PSP_PRESENT" {
present_pad_ids_by_layer
.entry(row.layer_id)
.or_default()
.insert(row.item_id.clone());
}
}
for (index, row) in presence_rows.iter().enumerate() {
if index >= REPORT_MAX_PRESENCE_ROWS {
continue;
}
out.push_str(&format!(
"- item_id={} layer_id={} layer_name={} presence={}\n",
row.item_id, row.layer_id, row.layer_name, row.presence
));
}
if presence_rows.len() > REPORT_MAX_PRESENCE_ROWS {
out.push_str(&format!(
"- ... omitted {} additional presence rows (use `padstack-presence` CLI command for full output)\n",
presence_rows.len() - REPORT_MAX_PRESENCE_ROWS
));
}
out.push('\n');
out.push_str("### Pad Shape Polygons (All Present Pad/Layer Pairs)\n\n");
out.push_str(
"For full per-node coordinate payloads, run `pad-shape-polygon --pad-id ... --layer-id ... --debug` for targeted pad/layer subsets.\n\n",
);
for layer in &enabled_layers {
let pad_ids_on_layer = present_pad_ids_by_layer
.get(&layer.id)
.map(|set| set.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();
out.push_str(&format!(
"#### Layer {} ({})\n\n- pad_count_present: {}\n\n",
layer.name,
layer.id,
pad_ids_on_layer.len()
));
if pad_ids_on_layer.is_empty() {
continue;
}
let polygons = client
.get_pad_shape_as_polygon(pad_ids_on_layer, layer.id)
.await?;
out.push_str(&format!("- polygon_entry_count: {}\n\n", polygons.len()));
for row in polygons {
let summary = polygon_geometry_summary(&row.polygon);
out.push_str(&format!(
"- pad_id={} layer_id={} layer_name={} outline_nodes={} hole_count={} hole_nodes_total={} point_nodes={} arc_nodes={}\n",
row.pad_id,
row.layer_id,
row.layer_name,
summary.outline_nodes,
summary.hole_count,
summary.hole_nodes_total,
summary.point_nodes,
summary.arc_nodes
));
}
out.push('\n');
}
out.push_str("## Board/Editor Raw Structures\n\n");
out.push_str("### Title Block\n\n");
let title_block = client.get_title_block_info().await?;
@ -864,7 +1149,16 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
}
out.push_str(&format!("- status: ok\n- count: {}\n\n", items.len()));
for (index, item) in items.iter().enumerate() {
for (index, item) in items
.iter()
.take(REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE)
.enumerate()
{
let mut debug = kicad_ipc::KiCadClient::debug_any_item(item)?;
if debug.len() > REPORT_MAX_ITEM_DEBUG_CHARS {
debug.truncate(REPORT_MAX_ITEM_DEBUG_CHARS);
debug.push_str("\n...<truncated; use items-raw CLI for full payload>");
}
out.push_str(&format!(
"#### item {}\n\n- type_url: `{}`\n- raw_len: `{}`\n\n",
index,
@ -872,9 +1166,17 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
item.value.len()
));
out.push_str("```text\n");
out.push_str(&kicad_ipc::KiCadClient::debug_any_item(item)?);
out.push_str(&debug);
out.push_str("\n```\n\n");
}
if items.len() > REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE {
out.push_str(&format!(
"- ... omitted {} additional item debug rows for {} (use `items-raw --type-id {}` for full output)\n\n",
items.len() - REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE,
object_type.name,
object_type.code
));
}
}
Err(err) => {
out.push_str(&format!("- status: error\n- error: `{}`\n\n", err));
@ -896,7 +1198,14 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
}
out.push_str("## Board File Snapshot (Raw)\n\n```scheme\n");
out.push_str(&client.get_board_as_string().await?);
let mut board_text = client.get_board_as_string().await?;
if board_text.len() > REPORT_MAX_BOARD_SNAPSHOT_CHARS {
board_text.truncate(REPORT_MAX_BOARD_SNAPSHOT_CHARS);
board_text.push_str(
"\n... ; <truncated board snapshot, rerun `board-as-string` command for full board text>\n",
);
}
out.push_str(&board_text);
out.push_str("\n```\n\n");
out.push_str("## Proto Coverage (Board Read)\n\n");
@ -954,13 +1263,13 @@ fn proto_coverage_board_read_rows() -> Vec<(&'static str, &'static str, &'static
),
(
"kiapi.board.commands.GetPadShapeAsPolygon",
"not-yet",
"pending",
"implemented",
"get_pad_shape_as_polygon/get_pad_shape_as_polygon_debug",
),
(
"kiapi.board.commands.CheckPadstackPresenceOnLayers",
"not-yet",
"pending",
"implemented",
"check_padstack_presence_on_layers/check_padstack_presence_on_layers_debug",
),
(
"kiapi.board.commands.GetVisibleLayers",
@ -1025,6 +1334,44 @@ fn proto_coverage_board_read_rows() -> Vec<(&'static str, &'static str, &'static
]
}
#[derive(Default)]
struct PolygonGeometrySummary {
outline_nodes: usize,
hole_count: usize,
hole_nodes_total: usize,
point_nodes: usize,
arc_nodes: usize,
}
fn polygon_geometry_summary(polygon: &kicad_ipc::PolygonWithHolesNm) -> PolygonGeometrySummary {
let mut summary = PolygonGeometrySummary {
hole_count: polygon.holes.len(),
..PolygonGeometrySummary::default()
};
if let Some(outline) = polygon.outline.as_ref() {
summary.outline_nodes = outline.nodes.len();
for node in &outline.nodes {
match node {
kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1,
kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1,
}
}
}
for hole in &polygon.holes {
summary.hole_nodes_total += hole.nodes.len();
for node in &hole.nodes {
match node {
kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1,
kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1,
}
}
}
summary
}
fn parse_item_ids(args: &[String], command_name: &str) -> Result<Vec<String>, KiCadError> {
let mut item_ids = Vec::new();
let mut i = 0;