feat: add board reconstruction verification report tooling
This commit is contained in:
parent
1b54e688c1
commit
59bc0e7838
File diff suppressed because it is too large
Load Diff
|
|
@ -125,6 +125,68 @@ Run hit-test on a specific item:
|
|||
cargo run --bin kicad-ipc-cli -- hit-test --id <uuid> --x-nm <x> --y-nm <y> --tolerance-nm 0
|
||||
```
|
||||
|
||||
List all PCB object type IDs from the proto enum:
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- types-pcb
|
||||
```
|
||||
|
||||
Dump raw item payloads for one or more PCB object type IDs:
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- items-raw --type-id 11 --type-id 13 --debug
|
||||
```
|
||||
|
||||
Dump raw payloads for all PCB object classes:
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- items-raw-all-pcb --debug
|
||||
```
|
||||
|
||||
Dump board text (KiCad s-expression):
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- board-as-string
|
||||
```
|
||||
|
||||
Dump selection text (KiCad s-expression):
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- selection-as-string
|
||||
```
|
||||
|
||||
Dump title block fields:
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- title-block
|
||||
```
|
||||
|
||||
Dump stackup/graphics/appearance raw debug:
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- stackup-debug
|
||||
cargo run --bin kicad-ipc-cli -- graphics-defaults-debug
|
||||
cargo run --bin kicad-ipc-cli -- appearance-debug
|
||||
```
|
||||
|
||||
Dump netclass map raw debug:
|
||||
|
||||
```bash
|
||||
cargo run --bin kicad-ipc-cli -- netclass-debug
|
||||
```
|
||||
|
||||
Print proto command coverage status (board read):
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
Get current project path (derived from open PCB docs):
|
||||
|
||||
```bash
|
||||
|
|
|
|||
574
src/client.rs
574
src/client.rs
|
|
@ -9,8 +9,9 @@ use crate::model::board::{
|
|||
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
||||
};
|
||||
use crate::model::common::{
|
||||
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, ProjectInfo,
|
||||
SelectionItemDetail, SelectionSummary, SelectionTypeCount, VersionInfo,
|
||||
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
|
||||
ProjectInfo, SelectionItemDetail, SelectionSummary, SelectionTypeCount, TitleBlockInfo,
|
||||
VersionInfo,
|
||||
};
|
||||
use crate::proto::kiapi::board::commands as board_commands;
|
||||
use crate::proto::kiapi::board::types as board_types;
|
||||
|
|
@ -29,11 +30,21 @@ const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabled
|
|||
const CMD_GET_ACTIVE_LAYER: &str = "kiapi.board.commands.GetActiveLayer";
|
||||
const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers";
|
||||
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
||||
const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup";
|
||||
const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults";
|
||||
const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
||||
"kiapi.board.commands.GetBoardEditorAppearanceSettings";
|
||||
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_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";
|
||||
const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox";
|
||||
const CMD_HIT_TEST: &str = "kiapi.common.commands.HitTest";
|
||||
const CMD_GET_TITLE_BLOCK_INFO: &str = "kiapi.common.commands.GetTitleBlockInfo";
|
||||
const CMD_SAVE_DOCUMENT_TO_STRING: &str = "kiapi.common.commands.SaveDocumentToString";
|
||||
const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionToString";
|
||||
|
||||
const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse";
|
||||
const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse";
|
||||
|
|
@ -41,11 +52,94 @@ const RES_GET_NETS: &str = "kiapi.board.commands.NetsResponse";
|
|||
const RES_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.BoardEnabledLayersResponse";
|
||||
const RES_BOARD_LAYER_RESPONSE: &str = "kiapi.board.commands.BoardLayerResponse";
|
||||
const RES_BOARD_LAYERS: &str = "kiapi.board.commands.BoardLayers";
|
||||
const RES_BOARD_STACKUP_RESPONSE: &str = "kiapi.board.commands.BoardStackupResponse";
|
||||
const RES_GRAPHICS_DEFAULTS_RESPONSE: &str = "kiapi.board.commands.GraphicsDefaultsResponse";
|
||||
const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
||||
"kiapi.board.commands.BoardEditorAppearanceSettings";
|
||||
const RES_NETCLASS_FOR_NETS_RESPONSE: &str = "kiapi.board.commands.NetClassForNetsResponse";
|
||||
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";
|
||||
const RES_GET_BOUNDING_BOX_RESPONSE: &str = "kiapi.common.commands.GetBoundingBoxResponse";
|
||||
const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse";
|
||||
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 PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbFootprint as i32,
|
||||
name: "KOT_PCB_FOOTPRINT",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbPad as i32,
|
||||
name: "KOT_PCB_PAD",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbShape as i32,
|
||||
name: "KOT_PCB_SHAPE",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbReferenceImage as i32,
|
||||
name: "KOT_PCB_REFERENCE_IMAGE",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbField as i32,
|
||||
name: "KOT_PCB_FIELD",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbGenerator as i32,
|
||||
name: "KOT_PCB_GENERATOR",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbText as i32,
|
||||
name: "KOT_PCB_TEXT",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbTextbox as i32,
|
||||
name: "KOT_PCB_TEXTBOX",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbTable as i32,
|
||||
name: "KOT_PCB_TABLE",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbTablecell as i32,
|
||||
name: "KOT_PCB_TABLECELL",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbTrace as i32,
|
||||
name: "KOT_PCB_TRACE",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbVia as i32,
|
||||
name: "KOT_PCB_VIA",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbArc as i32,
|
||||
name: "KOT_PCB_ARC",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbMarker as i32,
|
||||
name: "KOT_PCB_MARKER",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbDimension as i32,
|
||||
name: "KOT_PCB_DIMENSION",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbZone as i32,
|
||||
name: "KOT_PCB_ZONE",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbGroup as i32,
|
||||
name: "KOT_PCB_GROUP",
|
||||
},
|
||||
PcbObjectTypeCode {
|
||||
code: common_types::KiCadObjectType::KotPcbBarcode as i32,
|
||||
name: "KOT_PCB_BARCODE",
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KiCadClient {
|
||||
|
|
@ -350,6 +444,224 @@ impl KiCadClient {
|
|||
pad_netlist_from_footprint_items(footprint_items)
|
||||
}
|
||||
|
||||
pub fn pcb_object_type_codes() -> &'static [PcbObjectTypeCode] {
|
||||
&PCB_OBJECT_TYPES
|
||||
}
|
||||
|
||||
pub fn pcb_object_type_name(type_code: i32) -> Option<&'static str> {
|
||||
PCB_OBJECT_TYPES
|
||||
.iter()
|
||||
.find(|entry| entry.code == type_code)
|
||||
.map(|entry| entry.name)
|
||||
}
|
||||
|
||||
pub fn debug_any_item(item: &prost_types::Any) -> Result<String, KiCadError> {
|
||||
any_to_pretty_debug(item)
|
||||
}
|
||||
|
||||
pub async fn get_items_raw_by_type_codes(
|
||||
&self,
|
||||
type_codes: Vec<i32>,
|
||||
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||
self.get_items_raw(type_codes).await
|
||||
}
|
||||
|
||||
pub async fn get_items_details_by_type_codes(
|
||||
&self,
|
||||
type_codes: Vec<i32>,
|
||||
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||
let items = self.get_items_raw(type_codes).await?;
|
||||
summarize_item_details(items)
|
||||
}
|
||||
|
||||
pub async fn get_all_pcb_items_raw(
|
||||
&self,
|
||||
) -> Result<Vec<(PcbObjectTypeCode, Vec<prost_types::Any>)>, KiCadError> {
|
||||
let mut rows = Vec::with_capacity(PCB_OBJECT_TYPES.len());
|
||||
for object_type in PCB_OBJECT_TYPES {
|
||||
let items = self.get_items_raw(vec![object_type.code]).await?;
|
||||
rows.push((object_type, items));
|
||||
}
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn get_all_pcb_items_details(
|
||||
&self,
|
||||
) -> Result<Vec<(PcbObjectTypeCode, Vec<SelectionItemDetail>)>, KiCadError> {
|
||||
let mut rows = Vec::with_capacity(PCB_OBJECT_TYPES.len());
|
||||
for object_type in PCB_OBJECT_TYPES {
|
||||
let items = self.get_items_raw(vec![object_type.code]).await?;
|
||||
rows.push((object_type, summarize_item_details(items)?));
|
||||
}
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn get_items_by_net_raw(
|
||||
&self,
|
||||
type_codes: Vec<i32>,
|
||||
net_codes: Vec<i32>,
|
||||
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||
let command = board_commands::GetItemsByNet {
|
||||
header: Some(self.current_board_item_header().await?),
|
||||
types: type_codes,
|
||||
net_codes: net_codes
|
||||
.into_iter()
|
||||
.map(|value| board_types::NetCode { value })
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_NET))
|
||||
.await?;
|
||||
let payload: common_commands::GetItemsResponse =
|
||||
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||
ensure_item_request_ok(payload.status)?;
|
||||
Ok(payload.items)
|
||||
}
|
||||
|
||||
pub async fn get_items_by_net_class_raw(
|
||||
&self,
|
||||
type_codes: Vec<i32>,
|
||||
net_classes: Vec<String>,
|
||||
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||
let command = board_commands::GetItemsByNetClass {
|
||||
header: Some(self.current_board_item_header().await?),
|
||||
types: type_codes,
|
||||
net_classes,
|
||||
};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_NET_CLASS))
|
||||
.await?;
|
||||
let payload: common_commands::GetItemsResponse =
|
||||
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||
ensure_item_request_ok(payload.status)?;
|
||||
Ok(payload.items)
|
||||
}
|
||||
|
||||
pub async fn get_netclass_for_nets_debug(
|
||||
&self,
|
||||
nets: Vec<BoardNet>,
|
||||
) -> Result<String, KiCadError> {
|
||||
let command = board_commands::GetNetClassForNets {
|
||||
net: nets
|
||||
.into_iter()
|
||||
.map(|net| board_types::Net {
|
||||
code: Some(board_types::NetCode { value: net.code }),
|
||||
name: net.name,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_GET_NETCLASS_FOR_NETS))
|
||||
.await?;
|
||||
let payload: board_commands::NetClassForNetsResponse =
|
||||
envelope::unpack_any(&response, RES_NETCLASS_FOR_NETS_RESPONSE)?;
|
||||
Ok(format!("{:#?}", payload.classes))
|
||||
}
|
||||
|
||||
pub async fn get_board_stackup_debug(&self) -> Result<String, KiCadError> {
|
||||
let command = board_commands::GetBoardStackup {
|
||||
board: Some(self.current_board_document_proto().await?),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_GET_BOARD_STACKUP))
|
||||
.await?;
|
||||
let payload: board_commands::BoardStackupResponse =
|
||||
envelope::unpack_any(&response, RES_BOARD_STACKUP_RESPONSE)?;
|
||||
Ok(format!("{:#?}", payload.stackup))
|
||||
}
|
||||
|
||||
pub async fn get_graphics_defaults_debug(&self) -> Result<String, KiCadError> {
|
||||
let command = board_commands::GetGraphicsDefaults {
|
||||
board: Some(self.current_board_document_proto().await?),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_GET_GRAPHICS_DEFAULTS))
|
||||
.await?;
|
||||
let payload: board_commands::GraphicsDefaultsResponse =
|
||||
envelope::unpack_any(&response, RES_GRAPHICS_DEFAULTS_RESPONSE)?;
|
||||
Ok(format!("{:#?}", payload.defaults))
|
||||
}
|
||||
|
||||
pub async fn get_board_editor_appearance_settings_debug(&self) -> Result<String, KiCadError> {
|
||||
let command = board_commands::GetBoardEditorAppearanceSettings {};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(
|
||||
&command,
|
||||
CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS,
|
||||
))
|
||||
.await?;
|
||||
let payload: board_commands::BoardEditorAppearanceSettings =
|
||||
envelope::unpack_any(&response, RES_BOARD_EDITOR_APPEARANCE_SETTINGS)?;
|
||||
Ok(format!("{:#?}", payload))
|
||||
}
|
||||
|
||||
pub async fn get_title_block_info(&self) -> Result<TitleBlockInfo, KiCadError> {
|
||||
let command = common_commands::GetTitleBlockInfo {
|
||||
document: Some(self.current_board_document_proto().await?),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_GET_TITLE_BLOCK_INFO))
|
||||
.await?;
|
||||
let payload: common_types::TitleBlockInfo =
|
||||
envelope::unpack_any(&response, RES_TITLE_BLOCK_INFO)?;
|
||||
|
||||
let comments = vec![
|
||||
payload.comment1,
|
||||
payload.comment2,
|
||||
payload.comment3,
|
||||
payload.comment4,
|
||||
payload.comment5,
|
||||
payload.comment6,
|
||||
payload.comment7,
|
||||
payload.comment8,
|
||||
payload.comment9,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|comment| !comment.is_empty())
|
||||
.collect();
|
||||
|
||||
Ok(TitleBlockInfo {
|
||||
title: payload.title,
|
||||
date: payload.date,
|
||||
revision: payload.revision,
|
||||
company: payload.company,
|
||||
comments,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_board_as_string(&self) -> Result<String, KiCadError> {
|
||||
let command = common_commands::SaveDocumentToString {
|
||||
document: Some(self.current_board_document_proto().await?),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_SAVE_DOCUMENT_TO_STRING))
|
||||
.await?;
|
||||
let payload: common_commands::SavedDocumentResponse =
|
||||
envelope::unpack_any(&response, RES_SAVED_DOCUMENT_RESPONSE)?;
|
||||
Ok(payload.contents)
|
||||
}
|
||||
|
||||
pub async fn get_selection_as_string(&self) -> Result<String, KiCadError> {
|
||||
let command = common_commands::SaveSelectionToString {};
|
||||
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_SAVE_SELECTION_TO_STRING))
|
||||
.await?;
|
||||
let payload: common_commands::SavedSelectionResponse =
|
||||
envelope::unpack_any(&response, RES_SAVED_SELECTION_RESPONSE)?;
|
||||
Ok(payload.contents)
|
||||
}
|
||||
|
||||
pub async fn get_items_by_id_raw(
|
||||
&self,
|
||||
item_ids: Vec<String>,
|
||||
|
|
@ -764,6 +1076,49 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
|
|||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Arc") {
|
||||
let arc = decode_any::<board_types::Arc>(item, "kiapi.board.types.Arc")?;
|
||||
let id = arc.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
let start = arc
|
||||
.start
|
||||
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||
let mid = arc
|
||||
.mid
|
||||
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||
let end = arc
|
||||
.end
|
||||
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||
let width = arc
|
||||
.width
|
||||
.map_or_else(|| "-".to_string(), |w| w.value_nm.to_string());
|
||||
let layer = layer_to_model(arc.layer).name;
|
||||
let net = arc
|
||||
.net
|
||||
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
return Ok(format!(
|
||||
"arc id={id} start_nm={start} mid_nm={mid} end_nm={end} width_nm={width} layer={layer} net={net}"
|
||||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Via") {
|
||||
let via = decode_any::<board_types::Via>(item, "kiapi.board.types.Via")?;
|
||||
let id = via.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
let position = via
|
||||
.position
|
||||
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||
let net = via
|
||||
.net
|
||||
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let via_type = board_types::ViaType::try_from(via.r#type)
|
||||
.map(|value| value.as_str_name().to_string())
|
||||
.unwrap_or_else(|_| format!("UNKNOWN({})", via.r#type));
|
||||
return Ok(format!(
|
||||
"via id={id} pos_nm={position} type={via_type} net={net}"
|
||||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.FootprintInstance") {
|
||||
let fp = decode_any::<board_types::FootprintInstance>(
|
||||
item,
|
||||
|
|
@ -780,9 +1135,24 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
|
|||
let position = fp
|
||||
.position
|
||||
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||
let orientation_deg = fp.orientation.map_or_else(
|
||||
|| "-".to_string(),
|
||||
|orientation| orientation.value_degrees.to_string(),
|
||||
);
|
||||
let layer = layer_to_model(fp.layer).name;
|
||||
let pad_count = fp
|
||||
.definition
|
||||
.as_ref()
|
||||
.map(|definition| {
|
||||
definition
|
||||
.items
|
||||
.iter()
|
||||
.filter(|entry| entry.type_url == envelope::type_url("kiapi.board.types.Pad"))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
return Ok(format!(
|
||||
"footprint id={id} ref={reference} pos_nm={position} layer={layer}"
|
||||
"footprint id={id} ref={reference} pos_nm={position} orientation_deg={orientation_deg} layer={layer} pad_count={pad_count}"
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -800,6 +1170,50 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
|
|||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.BoardText") {
|
||||
let text = decode_any::<board_types::BoardText>(item, "kiapi.board.types.BoardText")?;
|
||||
let id = text.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
let layer = layer_to_model(text.layer).name;
|
||||
let body = text
|
||||
.text
|
||||
.as_ref()
|
||||
.map(|value| value.text.clone())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
return Ok(format!("text id={id} layer={layer} text={body}"));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") {
|
||||
let textbox =
|
||||
decode_any::<board_types::BoardTextBox>(item, "kiapi.board.types.BoardTextBox")?;
|
||||
let id = textbox.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
let layer = layer_to_model(textbox.layer).name;
|
||||
let body = textbox
|
||||
.textbox
|
||||
.as_ref()
|
||||
.map(|value| value.text.clone())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
return Ok(format!("textbox id={id} layer={layer} text={body}"));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Pad") {
|
||||
let pad = decode_any::<board_types::Pad>(item, "kiapi.board.types.Pad")?;
|
||||
let id = pad.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
let pad_type = board_types::PadType::try_from(pad.r#type)
|
||||
.map(|value| value.as_str_name().to_string())
|
||||
.unwrap_or_else(|_| format!("UNKNOWN({})", pad.r#type));
|
||||
let position = pad
|
||||
.position
|
||||
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||
let net = pad
|
||||
.net
|
||||
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
return Ok(format!(
|
||||
"pad id={id} number={} type={pad_type} pos_nm={position} net={net}",
|
||||
pad.number
|
||||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.BoardGraphicShape") {
|
||||
let shape = decode_any::<board_types::BoardGraphicShape>(
|
||||
item,
|
||||
|
|
@ -811,12 +1225,136 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
|
|||
.net
|
||||
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
return Ok(format!("graphic id={id} layer={layer} net={net}"));
|
||||
let geometry = shape
|
||||
.shape
|
||||
.as_ref()
|
||||
.map(|graphic| format!("{:?}", graphic.geometry))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
return Ok(format!(
|
||||
"graphic id={id} layer={layer} net={net} geometry={geometry}"
|
||||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Zone") {
|
||||
let zone = decode_any::<board_types::Zone>(item, "kiapi.board.types.Zone")?;
|
||||
let id = zone.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
let zone_type = board_types::ZoneType::try_from(zone.r#type)
|
||||
.map(|value| value.as_str_name().to_string())
|
||||
.unwrap_or_else(|_| format!("UNKNOWN({})", zone.r#type));
|
||||
return Ok(format!(
|
||||
"zone id={id} name={} type={} layer_count={} filled={} polygon_count={}",
|
||||
zone.name,
|
||||
zone_type,
|
||||
zone.layers.len(),
|
||||
zone.filled,
|
||||
zone.filled_polygons.len()
|
||||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Dimension") {
|
||||
let dimension = decode_any::<board_types::Dimension>(item, "kiapi.board.types.Dimension")?;
|
||||
let id = dimension.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
let layer = layer_to_model(dimension.layer).name;
|
||||
let text = dimension
|
||||
.text
|
||||
.as_ref()
|
||||
.map(|value| value.text.clone())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let style = format!("{:?}", dimension.dimension_style);
|
||||
return Ok(format!(
|
||||
"dimension id={id} layer={layer} text={} style={style}",
|
||||
text
|
||||
));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Group") {
|
||||
let group = decode_any::<board_types::Group>(item, "kiapi.board.types.Group")?;
|
||||
let id = group.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||
return Ok(format!(
|
||||
"group id={id} name={} item_count={}",
|
||||
group.name,
|
||||
group.items.len()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!("unparsed payload ({} bytes)", item.value.len()))
|
||||
}
|
||||
|
||||
fn any_to_pretty_debug(item: &prost_types::Any) -> Result<String, KiCadError> {
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Track") {
|
||||
let value = decode_any::<board_types::Track>(item, "kiapi.board.types.Track")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Arc") {
|
||||
let value = decode_any::<board_types::Arc>(item, "kiapi.board.types.Arc")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Via") {
|
||||
let value = decode_any::<board_types::Via>(item, "kiapi.board.types.Via")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.FootprintInstance") {
|
||||
let value = decode_any::<board_types::FootprintInstance>(
|
||||
item,
|
||||
"kiapi.board.types.FootprintInstance",
|
||||
)?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Pad") {
|
||||
let value = decode_any::<board_types::Pad>(item, "kiapi.board.types.Pad")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.BoardGraphicShape") {
|
||||
let value = decode_any::<board_types::BoardGraphicShape>(
|
||||
item,
|
||||
"kiapi.board.types.BoardGraphicShape",
|
||||
)?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.BoardText") {
|
||||
let value = decode_any::<board_types::BoardText>(item, "kiapi.board.types.BoardText")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") {
|
||||
let value =
|
||||
decode_any::<board_types::BoardTextBox>(item, "kiapi.board.types.BoardTextBox")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Field") {
|
||||
let value = decode_any::<board_types::Field>(item, "kiapi.board.types.Field")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Zone") {
|
||||
let value = decode_any::<board_types::Zone>(item, "kiapi.board.types.Zone")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Dimension") {
|
||||
let value = decode_any::<board_types::Dimension>(item, "kiapi.board.types.Dimension")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
if item.type_url == envelope::type_url("kiapi.board.types.Group") {
|
||||
let value = decode_any::<board_types::Group>(item, "kiapi.board.types.Group")?;
|
||||
return Ok(format!("{:#?}", value));
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"unparsed_any type_url={} raw_len={}",
|
||||
item.type_url,
|
||||
item.value.len()
|
||||
))
|
||||
}
|
||||
|
||||
fn select_single_board_document(
|
||||
docs: &[DocumentSpecifier],
|
||||
) -> Result<&DocumentSpecifier, KiCadError> {
|
||||
|
|
@ -934,10 +1472,10 @@ fn default_client_name() -> String {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
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,
|
||||
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,
|
||||
};
|
||||
use crate::error::KiCadError;
|
||||
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
|
||||
|
|
@ -1281,4 +1819,24 @@ mod tests {
|
|||
crate::model::common::ItemHitTestResult::NoHit
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcb_object_type_catalog_contains_expected_trace_entry() {
|
||||
assert!(PCB_OBJECT_TYPES
|
||||
.iter()
|
||||
.any(|entry| entry.name == "KOT_PCB_TRACE" && entry.code == 11));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_to_pretty_debug_handles_unknown_type_without_error() {
|
||||
let unknown = prost_types::Any {
|
||||
type_url: "type.googleapis.com/kiapi.board.types.DoesNotExist".to_string(),
|
||||
value: vec![0xde, 0xad, 0xbe, 0xef],
|
||||
};
|
||||
|
||||
let debug = any_to_pretty_debug(&unknown)
|
||||
.expect("unknown Any payload type should not fail debug rendering");
|
||||
assert!(debug.contains("unparsed_any"));
|
||||
assert!(debug.contains("raw_len=4"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ pub use crate::model::board::{
|
|||
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
||||
};
|
||||
pub use crate::model::common::{
|
||||
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, SelectionItemDetail,
|
||||
SelectionSummary, SelectionTypeCount, VersionInfo,
|
||||
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
|
||||
SelectionItemDetail, SelectionSummary, SelectionTypeCount, TitleBlockInfo, VersionInfo,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -112,6 +112,15 @@ pub struct SelectionItemDetail {
|
|||
pub raw_len: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TitleBlockInfo {
|
||||
pub title: String,
|
||||
pub date: String,
|
||||
pub revision: String,
|
||||
pub company: String,
|
||||
pub comments: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ItemBoundingBox {
|
||||
pub item_id: String,
|
||||
|
|
@ -128,6 +137,12 @@ pub enum ItemHitTestResult {
|
|||
Hit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PcbObjectTypeCode {
|
||||
pub code: i32,
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ItemHitTestResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = match self {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use kicad_ipc::{BoardOriginKind, ClientBuilder, DocumentType, KiCadError, Vector2Nm};
|
||||
use kicad_ipc::{
|
||||
BoardOriginKind, ClientBuilder, DocumentType, KiCadClient, KiCadError, PcbObjectTypeCode,
|
||||
Vector2Nm,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CliConfig {
|
||||
|
|
@ -44,6 +49,25 @@ enum Command {
|
|||
y_nm: i64,
|
||||
tolerance_nm: i32,
|
||||
},
|
||||
PcbTypes,
|
||||
ItemsRaw {
|
||||
type_codes: Vec<i32>,
|
||||
include_debug: bool,
|
||||
},
|
||||
ItemsRawAllPcb {
|
||||
include_debug: bool,
|
||||
},
|
||||
TitleBlock,
|
||||
BoardAsString,
|
||||
SelectionAsString,
|
||||
StackupDebug,
|
||||
GraphicsDefaultsDebug,
|
||||
AppearanceDebug,
|
||||
NetClassDebug,
|
||||
BoardReadReport {
|
||||
output: PathBuf,
|
||||
},
|
||||
ProtoCoverageBoardRead,
|
||||
Smoke,
|
||||
Help,
|
||||
}
|
||||
|
|
@ -262,6 +286,134 @@ async fn run() -> Result<(), KiCadError> {
|
|||
.await?;
|
||||
println!("hit_test={result}");
|
||||
}
|
||||
Command::PcbTypes => {
|
||||
for entry in kicad_ipc::KiCadClient::pcb_object_type_codes() {
|
||||
println!("type_id={} type_name={}", entry.code, entry.name);
|
||||
}
|
||||
}
|
||||
Command::ItemsRaw {
|
||||
type_codes,
|
||||
include_debug,
|
||||
} => {
|
||||
let items = client
|
||||
.get_items_raw_by_type_codes(type_codes.clone())
|
||||
.await?;
|
||||
println!(
|
||||
"items_total={} requested_type_codes={:?}",
|
||||
items.len(),
|
||||
type_codes
|
||||
);
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
if include_debug {
|
||||
let debug = kicad_ipc::KiCadClient::debug_any_item(item)?
|
||||
.replace('\n', "\\n")
|
||||
.replace('\t', " ");
|
||||
println!(
|
||||
"[{index}] type_url={} raw_len={} raw_hex={} debug={}",
|
||||
item.type_url,
|
||||
item.value.len(),
|
||||
bytes_to_hex(&item.value),
|
||||
debug
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"[{index}] type_url={} raw_len={} raw_hex={}",
|
||||
item.type_url,
|
||||
item.value.len(),
|
||||
bytes_to_hex(&item.value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::ItemsRawAllPcb { include_debug } => {
|
||||
for object_type in kicad_ipc::KiCadClient::pcb_object_type_codes() {
|
||||
match client
|
||||
.get_items_raw_by_type_codes(vec![object_type.code])
|
||||
.await
|
||||
{
|
||||
Ok(items) => {
|
||||
println!(
|
||||
"type_id={} type_name={} item_count={}",
|
||||
object_type.code,
|
||||
object_type.name,
|
||||
items.len()
|
||||
);
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
if include_debug {
|
||||
let debug = kicad_ipc::KiCadClient::debug_any_item(item)?
|
||||
.replace('\n', "\\n")
|
||||
.replace('\t', " ");
|
||||
println!(
|
||||
" [{index}] type_url={} raw_len={} raw_hex={} debug={}",
|
||||
item.type_url,
|
||||
item.value.len(),
|
||||
bytes_to_hex(&item.value),
|
||||
debug
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" [{index}] type_url={} raw_len={} raw_hex={}",
|
||||
item.type_url,
|
||||
item.value.len(),
|
||||
bytes_to_hex(&item.value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!(
|
||||
"type_id={} type_name={} error={}",
|
||||
object_type.code, object_type.name, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::TitleBlock => {
|
||||
let title_block = client.get_title_block_info().await?;
|
||||
println!("title={}", title_block.title);
|
||||
println!("date={}", title_block.date);
|
||||
println!("revision={}", title_block.revision);
|
||||
println!("company={}", title_block.company);
|
||||
for (index, comment) in title_block.comments.iter().enumerate() {
|
||||
println!("comment{}={}", index + 1, comment);
|
||||
}
|
||||
}
|
||||
Command::BoardAsString => {
|
||||
let content = client.get_board_as_string().await?;
|
||||
println!("{content}");
|
||||
}
|
||||
Command::SelectionAsString => {
|
||||
let content = client.get_selection_as_string().await?;
|
||||
println!("{content}");
|
||||
}
|
||||
Command::StackupDebug => {
|
||||
let debug = client.get_board_stackup_debug().await?;
|
||||
println!("{debug}");
|
||||
}
|
||||
Command::GraphicsDefaultsDebug => {
|
||||
let debug = client.get_graphics_defaults_debug().await?;
|
||||
println!("{debug}");
|
||||
}
|
||||
Command::AppearanceDebug => {
|
||||
let debug = client.get_board_editor_appearance_settings_debug().await?;
|
||||
println!("{debug}");
|
||||
}
|
||||
Command::NetClassDebug => {
|
||||
let nets = client.get_nets().await?;
|
||||
let debug = client.get_netclass_for_nets_debug(nets).await?;
|
||||
println!("{debug}");
|
||||
}
|
||||
Command::BoardReadReport { output } => {
|
||||
let report = build_board_read_report_markdown(&client).await?;
|
||||
fs::write(&output, report).map_err(|err| KiCadError::Config {
|
||||
reason: format!("failed to write report to `{}`: {err}", output.display()),
|
||||
})?;
|
||||
println!("wrote_report={}", output.display());
|
||||
}
|
||||
Command::ProtoCoverageBoardRead => {
|
||||
print_proto_coverage_board_read();
|
||||
}
|
||||
Command::Smoke => {
|
||||
client.ping().await?;
|
||||
let version = client.get_version().await?;
|
||||
|
|
@ -452,6 +604,74 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> {
|
|||
tolerance_nm,
|
||||
}
|
||||
}
|
||||
"types-pcb" => Command::PcbTypes,
|
||||
"items-raw" => {
|
||||
let mut type_codes = Vec::new();
|
||||
let mut include_debug = false;
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--type-id" => {
|
||||
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||
reason: "missing value for items-raw --type-id".to_string(),
|
||||
})?;
|
||||
type_codes.push(value.parse::<i32>().map_err(|err| {
|
||||
KiCadError::Config {
|
||||
reason: format!("invalid items-raw --type-id `{value}`: {err}"),
|
||||
}
|
||||
})?);
|
||||
i += 2;
|
||||
}
|
||||
"--debug" => {
|
||||
include_debug = true;
|
||||
i += 1;
|
||||
}
|
||||
_ => {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if type_codes.is_empty() {
|
||||
return Err(KiCadError::Config {
|
||||
reason: "items-raw requires one or more `--type-id <i32>` arguments"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Command::ItemsRaw {
|
||||
type_codes,
|
||||
include_debug,
|
||||
}
|
||||
}
|
||||
"items-raw-all-pcb" => {
|
||||
let include_debug = args.iter().any(|arg| arg == "--debug");
|
||||
Command::ItemsRawAllPcb { include_debug }
|
||||
}
|
||||
"title-block" => Command::TitleBlock,
|
||||
"board-as-string" => Command::BoardAsString,
|
||||
"selection-as-string" => Command::SelectionAsString,
|
||||
"stackup-debug" => Command::StackupDebug,
|
||||
"graphics-defaults-debug" => Command::GraphicsDefaultsDebug,
|
||||
"appearance-debug" => Command::AppearanceDebug,
|
||||
"netclass-debug" => Command::NetClassDebug,
|
||||
"proto-coverage-board-read" => Command::ProtoCoverageBoardRead,
|
||||
"board-read-report" => {
|
||||
let mut output = PathBuf::from("docs/BOARD_READ_REPORT.md");
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
if args[i] == "--out" {
|
||||
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||
reason: "missing value for board-read-report --out".to_string(),
|
||||
})?;
|
||||
output = PathBuf::from(value);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Command::BoardReadReport { output }
|
||||
}
|
||||
"smoke" => Command::Smoke,
|
||||
"open-docs" => {
|
||||
let mut document_type = DocumentType::Pcb;
|
||||
|
|
@ -490,10 +710,321 @@ fn default_config() -> CliConfig {
|
|||
|
||||
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 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 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"
|
||||
);
|
||||
}
|
||||
|
||||
async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String, KiCadError> {
|
||||
let mut out = String::new();
|
||||
out.push_str("# Board Read Reconstruction Report\n\n");
|
||||
out.push_str("Generated by `kicad-ipc-cli board-read-report`.\n\n");
|
||||
out.push_str("Goal: verify that non-mutating PCB API reads are sufficient to reconstruct board state.\n\n");
|
||||
|
||||
let version = client.get_version().await?;
|
||||
out.push_str("## Session\n\n");
|
||||
out.push_str(&format!(
|
||||
"- KiCad version: {}.{}.{} ({})\n",
|
||||
version.major, version.minor, version.patch, version.full_version
|
||||
));
|
||||
out.push_str(&format!("- Socket URI: `{}`\n", client.socket_uri()));
|
||||
out.push_str(&format!(
|
||||
"- Timeout (ms): {}\n\n",
|
||||
client.timeout().as_millis()
|
||||
));
|
||||
|
||||
out.push_str("## Open Documents\n\n");
|
||||
let docs = client.get_open_documents(DocumentType::Pcb).await?;
|
||||
if docs.is_empty() {
|
||||
out.push_str("- No open PCB docs\n\n");
|
||||
} else {
|
||||
for (index, doc) in docs.iter().enumerate() {
|
||||
out.push_str(&format!(
|
||||
"- [{}] type={} board={} project_name={} project_path={}\n",
|
||||
index,
|
||||
doc.document_type,
|
||||
doc.board_filename.as_deref().unwrap_or("-"),
|
||||
doc.project.name.as_deref().unwrap_or("-"),
|
||||
doc.project
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "-".to_string())
|
||||
));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
out.push_str("## Layer / Origin / Nets\n\n");
|
||||
let enabled = client.get_board_enabled_layers().await?;
|
||||
out.push_str(&format!(
|
||||
"- copper_layer_count: {}\n",
|
||||
enabled.copper_layer_count
|
||||
));
|
||||
out.push_str("- enabled_layers:\n");
|
||||
for layer in enabled.layers {
|
||||
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
|
||||
}
|
||||
|
||||
let visible_layers = client.get_visible_layers().await?;
|
||||
out.push_str("- visible_layers:\n");
|
||||
for layer in visible_layers {
|
||||
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
|
||||
}
|
||||
|
||||
let active_layer = client.get_active_layer().await?;
|
||||
out.push_str(&format!(
|
||||
"- active_layer: {} ({})\n",
|
||||
active_layer.name, active_layer.id
|
||||
));
|
||||
|
||||
let grid_origin = client
|
||||
.get_board_origin(kicad_ipc::BoardOriginKind::Grid)
|
||||
.await?;
|
||||
out.push_str(&format!(
|
||||
"- grid_origin_nm: {},{}\n",
|
||||
grid_origin.x_nm, grid_origin.y_nm
|
||||
));
|
||||
let drill_origin = client
|
||||
.get_board_origin(kicad_ipc::BoardOriginKind::Drill)
|
||||
.await?;
|
||||
out.push_str(&format!(
|
||||
"- drill_origin_nm: {},{}\n",
|
||||
drill_origin.x_nm, drill_origin.y_nm
|
||||
));
|
||||
|
||||
let nets = client.get_nets().await?;
|
||||
out.push_str(&format!("- net_count: {}\n", nets.len()));
|
||||
out.push_str("\n### Netlist\n\n");
|
||||
for net in &nets {
|
||||
out.push_str(&format!("- code={} name={}\n", net.code, net.name));
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
out.push_str("### Pad-Level Netlist (Footprint/Pad/Net)\n\n");
|
||||
let pad_entries = client.get_pad_netlist().await?;
|
||||
out.push_str(&format!("- pad_entry_count: {}\n", pad_entries.len()));
|
||||
for entry in pad_entries {
|
||||
out.push_str(&format!(
|
||||
"- footprint_ref={} footprint_id={} pad_id={} pad_number={} net_code={} net_name={}\n",
|
||||
entry.footprint_reference.as_deref().unwrap_or("-"),
|
||||
entry.footprint_id.as_deref().unwrap_or("-"),
|
||||
entry.pad_id.as_deref().unwrap_or("-"),
|
||||
entry.pad_number,
|
||||
entry
|
||||
.net_code
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
entry.net_name.as_deref().unwrap_or("-")
|
||||
));
|
||||
}
|
||||
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?;
|
||||
out.push_str(&format!("- title: {}\n", title_block.title));
|
||||
out.push_str(&format!("- date: {}\n", title_block.date));
|
||||
out.push_str(&format!("- revision: {}\n", title_block.revision));
|
||||
out.push_str(&format!("- company: {}\n", title_block.company));
|
||||
for (index, comment) in title_block.comments.iter().enumerate() {
|
||||
out.push_str(&format!("- comment{}: {}\n", index + 1, comment));
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
out.push_str("### Stackup (Raw Debug)\n\n```text\n");
|
||||
out.push_str(&client.get_board_stackup_debug().await?);
|
||||
out.push_str("\n```\n\n");
|
||||
|
||||
out.push_str("### Graphics Defaults (Raw Debug)\n\n```text\n");
|
||||
out.push_str(&client.get_graphics_defaults_debug().await?);
|
||||
out.push_str("\n```\n\n");
|
||||
|
||||
out.push_str("### Editor Appearance (Raw Debug)\n\n```text\n");
|
||||
out.push_str(&client.get_board_editor_appearance_settings_debug().await?);
|
||||
out.push_str("\n```\n\n");
|
||||
|
||||
out.push_str("### NetClass Map (Raw Debug)\n\n```text\n");
|
||||
out.push_str(&client.get_netclass_for_nets_debug(nets).await?);
|
||||
out.push_str("\n```\n\n");
|
||||
|
||||
out.push_str("## PCB Item Coverage (All KOT_PCB_* Types)\n\n");
|
||||
let mut missing_types: Vec<PcbObjectTypeCode> = Vec::new();
|
||||
for object_type in kicad_ipc::KiCadClient::pcb_object_type_codes() {
|
||||
out.push_str(&format!(
|
||||
"### {} ({})\n\n",
|
||||
object_type.name, object_type.code
|
||||
));
|
||||
match client
|
||||
.get_items_raw_by_type_codes(vec![object_type.code])
|
||||
.await
|
||||
{
|
||||
Ok(items) => {
|
||||
if items.is_empty() {
|
||||
missing_types.push(*object_type);
|
||||
}
|
||||
out.push_str(&format!("- status: ok\n- count: {}\n\n", items.len()));
|
||||
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
out.push_str(&format!(
|
||||
"#### item {}\n\n- type_url: `{}`\n- raw_len: `{}`\n\n",
|
||||
index,
|
||||
item.type_url,
|
||||
item.value.len()
|
||||
));
|
||||
out.push_str("```text\n");
|
||||
out.push_str(&kicad_ipc::KiCadClient::debug_any_item(item)?);
|
||||
out.push_str("\n```\n\n");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
out.push_str(&format!("- status: error\n- error: `{}`\n\n", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str("## Missing Item Classes In Current Board\n\n");
|
||||
if missing_types.is_empty() {
|
||||
out.push_str("- none\n\n");
|
||||
} else {
|
||||
for object_type in missing_types {
|
||||
out.push_str(&format!(
|
||||
"- {} ({}) had zero items in this board\n",
|
||||
object_type.name, object_type.code
|
||||
));
|
||||
}
|
||||
out.push_str("\nIf these are important for your reconstruction target, open a denser board and rerun this report.\n\n");
|
||||
}
|
||||
|
||||
out.push_str("## Board File Snapshot (Raw)\n\n```scheme\n");
|
||||
out.push_str(&client.get_board_as_string().await?);
|
||||
out.push_str("\n```\n\n");
|
||||
|
||||
out.push_str("## Proto Coverage (Board Read)\n\n");
|
||||
for (command, status, note) in proto_coverage_board_read_rows() {
|
||||
out.push_str(&format!("- `{}` -> `{}` ({})\n", command, status, note));
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn print_proto_coverage_board_read() {
|
||||
for (command, status, note) in proto_coverage_board_read_rows() {
|
||||
println!("command={} status={} note={}", command, status, note);
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_coverage_board_read_rows() -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
vec![
|
||||
(
|
||||
"kiapi.board.commands.GetBoardStackup",
|
||||
"implemented",
|
||||
"get_board_stackup_debug",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetBoardEnabledLayers",
|
||||
"implemented",
|
||||
"get_board_enabled_layers",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetGraphicsDefaults",
|
||||
"implemented",
|
||||
"get_graphics_defaults_debug",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetBoardOrigin",
|
||||
"implemented",
|
||||
"get_board_origin",
|
||||
),
|
||||
("kiapi.board.commands.GetNets", "implemented", "get_nets"),
|
||||
(
|
||||
"kiapi.board.commands.GetItemsByNet",
|
||||
"implemented",
|
||||
"get_items_by_net_raw",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetItemsByNetClass",
|
||||
"implemented",
|
||||
"get_items_by_net_class_raw",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetNetClassForNets",
|
||||
"implemented",
|
||||
"get_netclass_for_nets_debug",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetPadShapeAsPolygon",
|
||||
"not-yet",
|
||||
"pending",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.CheckPadstackPresenceOnLayers",
|
||||
"not-yet",
|
||||
"pending",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetVisibleLayers",
|
||||
"implemented",
|
||||
"get_visible_layers",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetActiveLayer",
|
||||
"implemented",
|
||||
"get_active_layer",
|
||||
),
|
||||
(
|
||||
"kiapi.board.commands.GetBoardEditorAppearanceSettings",
|
||||
"implemented",
|
||||
"get_board_editor_appearance_settings_debug",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.GetOpenDocuments",
|
||||
"implemented",
|
||||
"get_open_documents",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.GetItems",
|
||||
"implemented",
|
||||
"get_items_raw_by_type_codes",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.GetItemsById",
|
||||
"implemented",
|
||||
"get_items_by_id_raw",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.GetBoundingBox",
|
||||
"implemented",
|
||||
"get_item_bounding_boxes",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.GetSelection",
|
||||
"implemented",
|
||||
"get_selection_raw/get_selection_details",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.HitTest",
|
||||
"implemented",
|
||||
"hit_test_item",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.GetTitleBlockInfo",
|
||||
"implemented",
|
||||
"get_title_block_info",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.SaveDocumentToString",
|
||||
"implemented",
|
||||
"get_board_as_string",
|
||||
),
|
||||
(
|
||||
"kiapi.common.commands.SaveSelectionToString",
|
||||
"implemented",
|
||||
"get_selection_as_string",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn parse_item_ids(args: &[String], command_name: &str) -> Result<Vec<String>, KiCadError> {
|
||||
let mut item_ids = Vec::new();
|
||||
let mut i = 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue