feat: add selection detail and pad netlist APIs
This commit is contained in:
parent
7e76a1396d
commit
08c2b6f919
|
|
@ -83,6 +83,24 @@ Show summary of current PCB selection by item type:
|
||||||
cargo run --bin kicad-ipc-cli -- selection-summary
|
cargo run --bin kicad-ipc-cli -- selection-summary
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Show parsed details for currently selected items:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin kicad-ipc-cli -- selection-details
|
||||||
|
```
|
||||||
|
|
||||||
|
Show raw protobuf payload bytes for selected items:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin kicad-ipc-cli -- selection-raw
|
||||||
|
```
|
||||||
|
|
||||||
|
Show pad-level netlist entries (footprint/pad/net):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin kicad-ipc-cli -- netlist-pads
|
||||||
|
```
|
||||||
|
|
||||||
Get current project path (derived from open PCB docs):
|
Get current project path (derived from open PCB docs):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
352
src/client.rs
352
src/client.rs
|
|
@ -6,10 +6,11 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use crate::envelope;
|
use crate::envelope;
|
||||||
use crate::error::KiCadError;
|
use crate::error::KiCadError;
|
||||||
use crate::model::board::{
|
use crate::model::board::{
|
||||||
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, Vector2Nm,
|
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
||||||
};
|
};
|
||||||
use crate::model::common::{
|
use crate::model::common::{
|
||||||
DocumentSpecifier, DocumentType, ProjectInfo, SelectionSummary, SelectionTypeCount, VersionInfo,
|
DocumentSpecifier, DocumentType, ProjectInfo, SelectionItemDetail, SelectionSummary,
|
||||||
|
SelectionTypeCount, VersionInfo,
|
||||||
};
|
};
|
||||||
use crate::proto::kiapi::board::commands as board_commands;
|
use crate::proto::kiapi::board::commands as board_commands;
|
||||||
use crate::proto::kiapi::board::types as board_types;
|
use crate::proto::kiapi::board::types as board_types;
|
||||||
|
|
@ -29,6 +30,7 @@ const CMD_GET_ACTIVE_LAYER: &str = "kiapi.board.commands.GetActiveLayer";
|
||||||
const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers";
|
const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers";
|
||||||
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
||||||
const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
|
const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
|
||||||
|
const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems";
|
||||||
|
|
||||||
const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse";
|
const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse";
|
||||||
const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse";
|
const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse";
|
||||||
|
|
@ -38,6 +40,7 @@ const RES_BOARD_LAYER_RESPONSE: &str = "kiapi.board.commands.BoardLayerResponse"
|
||||||
const RES_BOARD_LAYERS: &str = "kiapi.board.commands.BoardLayers";
|
const RES_BOARD_LAYERS: &str = "kiapi.board.commands.BoardLayers";
|
||||||
const RES_VECTOR2: &str = "kiapi.common.types.Vector2";
|
const RES_VECTOR2: &str = "kiapi.common.types.Vector2";
|
||||||
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
|
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
|
||||||
|
const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse";
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KiCadClient {
|
pub struct KiCadClient {
|
||||||
|
|
@ -314,6 +317,51 @@ impl KiCadClient {
|
||||||
Ok(summarize_selection(payload.items))
|
Ok(summarize_selection(payload.items))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let document = self.current_board_document_proto().await?;
|
||||||
|
let command = common_commands::GetSelection {
|
||||||
|
header: Some(common_types::ItemHeader {
|
||||||
|
document: Some(document),
|
||||||
|
container: None,
|
||||||
|
field_mask: None,
|
||||||
|
}),
|
||||||
|
types: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_SELECTION))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::SelectionResponse =
|
||||||
|
envelope::unpack_any(&response, RES_SELECTION_RESPONSE)?;
|
||||||
|
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_selection_details(&self) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||||
|
let items = self.get_selection_raw().await?;
|
||||||
|
let mut details = Vec::with_capacity(items.len());
|
||||||
|
for item in items {
|
||||||
|
let raw_len = item.value.len();
|
||||||
|
let type_url = item.type_url.clone();
|
||||||
|
let detail = selection_item_detail(&item)?;
|
||||||
|
details.push(SelectionItemDetail {
|
||||||
|
type_url,
|
||||||
|
detail,
|
||||||
|
raw_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError> {
|
||||||
|
let footprint_items = self
|
||||||
|
.get_items_raw(vec![common_types::KiCadObjectType::KotPcbFootprint as i32])
|
||||||
|
.await?;
|
||||||
|
pad_netlist_from_footprint_items(footprint_items)
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_command(
|
async fn send_command(
|
||||||
&self,
|
&self,
|
||||||
command: prost_types::Any,
|
command: prost_types::Any,
|
||||||
|
|
@ -357,6 +405,36 @@ impl KiCadClient {
|
||||||
let selected = select_single_board_document(&docs)?;
|
let selected = select_single_board_document(&docs)?;
|
||||||
Ok(model_document_to_proto(selected))
|
Ok(model_document_to_proto(selected))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_items_raw(&self, types: Vec<i32>) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let document = self.current_board_document_proto().await?;
|
||||||
|
let command = common_commands::GetItems {
|
||||||
|
header: Some(common_types::ItemHeader {
|
||||||
|
document: Some(document),
|
||||||
|
container: None,
|
||||||
|
field_mask: None,
|
||||||
|
}),
|
||||||
|
types,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetItemsResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||||
|
|
||||||
|
let request_status = common_types::ItemRequestStatus::try_from(payload.status)
|
||||||
|
.unwrap_or(common_types::ItemRequestStatus::IrsUnknown);
|
||||||
|
|
||||||
|
if request_status != common_types::ItemRequestStatus::IrsOk {
|
||||||
|
return Err(KiCadError::ItemStatus {
|
||||||
|
code: request_status.as_str_name().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_document_specifier(source: common_types::DocumentSpecifier) -> Option<DocumentSpecifier> {
|
fn map_document_specifier(source: common_types::DocumentSpecifier) -> Option<DocumentSpecifier> {
|
||||||
|
|
@ -444,6 +522,156 @@ fn summarize_selection(items: Vec<prost_types::Any>) -> SelectionSummary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_any<T: prost::Message + Default>(
|
||||||
|
payload: &prost_types::Any,
|
||||||
|
expected_type_name: &str,
|
||||||
|
) -> Result<T, KiCadError> {
|
||||||
|
let expected_type_url = envelope::type_url(expected_type_name);
|
||||||
|
if payload.type_url != expected_type_url {
|
||||||
|
return Err(KiCadError::UnexpectedPayloadType {
|
||||||
|
expected_type_url,
|
||||||
|
actual_type_url: payload.type_url.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
T::decode(payload.value.as_slice()).map_err(|err| KiCadError::ProtobufDecode(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_netlist_from_footprint_items(
|
||||||
|
footprint_items: Vec<prost_types::Any>,
|
||||||
|
) -> Result<Vec<PadNetEntry>, KiCadError> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for item in footprint_items {
|
||||||
|
if item.type_url != envelope::type_url("kiapi.board.types.FootprintInstance") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let footprint = decode_any::<board_types::FootprintInstance>(
|
||||||
|
&item,
|
||||||
|
"kiapi.board.types.FootprintInstance",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let footprint_reference = footprint
|
||||||
|
.reference_field
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|field| field.text.as_ref())
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
|
||||||
|
let footprint_id = footprint.id.as_ref().map(|id| id.value.clone());
|
||||||
|
|
||||||
|
let footprint_definition = footprint.definition.unwrap_or_default();
|
||||||
|
for sub_item in footprint_definition.items {
|
||||||
|
if sub_item.type_url != envelope::type_url("kiapi.board.types.Pad") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pad = decode_any::<board_types::Pad>(&sub_item, "kiapi.board.types.Pad")?;
|
||||||
|
let (net_code, net_name) = match pad.net {
|
||||||
|
Some(net) => {
|
||||||
|
let code = net.code.map(|code| code.value);
|
||||||
|
let name = if net.name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(net.name)
|
||||||
|
};
|
||||||
|
(code, name)
|
||||||
|
}
|
||||||
|
None => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
entries.push(PadNetEntry {
|
||||||
|
footprint_reference: footprint_reference.clone(),
|
||||||
|
footprint_id: footprint_id.clone(),
|
||||||
|
pad_id: pad.id.map(|id| id.value),
|
||||||
|
pad_number: pad.number,
|
||||||
|
net_code,
|
||||||
|
net_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError> {
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Track") {
|
||||||
|
let track = decode_any::<board_types::Track>(item, "kiapi.board.types.Track")?;
|
||||||
|
let id = track.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let start = track
|
||||||
|
.start
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let end = track
|
||||||
|
.end
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let width = track
|
||||||
|
.width
|
||||||
|
.map_or_else(|| "-".to_string(), |w| w.value_nm.to_string());
|
||||||
|
let layer = layer_to_model(track.layer).name;
|
||||||
|
let net = track
|
||||||
|
.net
|
||||||
|
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
|
||||||
|
return Ok(format!(
|
||||||
|
"track id={id} start_nm={start} end_nm={end} width_nm={width} layer={layer} net={net}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.FootprintInstance") {
|
||||||
|
let fp = decode_any::<board_types::FootprintInstance>(
|
||||||
|
item,
|
||||||
|
"kiapi.board.types.FootprintInstance",
|
||||||
|
)?;
|
||||||
|
let id = fp.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let reference = fp
|
||||||
|
.reference_field
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|field| field.text.as_ref())
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
let position = fp
|
||||||
|
.position
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let layer = layer_to_model(fp.layer).name;
|
||||||
|
return Ok(format!(
|
||||||
|
"footprint id={id} ref={reference} pos_nm={position} layer={layer}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Field") {
|
||||||
|
let field = decode_any::<board_types::Field>(item, "kiapi.board.types.Field")?;
|
||||||
|
let text = field
|
||||||
|
.text
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
return Ok(format!(
|
||||||
|
"field name={} visible={} text={}",
|
||||||
|
field.name, field.visible, text
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.BoardGraphicShape") {
|
||||||
|
let shape = decode_any::<board_types::BoardGraphicShape>(
|
||||||
|
item,
|
||||||
|
"kiapi.board.types.BoardGraphicShape",
|
||||||
|
)?;
|
||||||
|
let id = shape.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let layer = layer_to_model(shape.layer).name;
|
||||||
|
let net = shape
|
||||||
|
.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}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!("unparsed payload ({} bytes)", item.value.len()))
|
||||||
|
}
|
||||||
|
|
||||||
fn select_single_board_document(
|
fn select_single_board_document(
|
||||||
docs: &[DocumentSpecifier],
|
docs: &[DocumentSpecifier],
|
||||||
) -> Result<&DocumentSpecifier, KiCadError> {
|
) -> Result<&DocumentSpecifier, KiCadError> {
|
||||||
|
|
@ -562,10 +790,12 @@ fn default_client_name() -> String {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
layer_to_model, model_document_to_proto, normalize_socket_uri,
|
layer_to_model, model_document_to_proto, normalize_socket_uri,
|
||||||
select_single_board_document, select_single_project_path, summarize_selection,
|
pad_netlist_from_footprint_items, select_single_board_document, select_single_project_path,
|
||||||
|
selection_item_detail, summarize_selection,
|
||||||
};
|
};
|
||||||
use crate::error::KiCadError;
|
use crate::error::KiCadError;
|
||||||
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
|
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
|
||||||
|
use prost::Message;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -726,4 +956,120 @@ mod tests {
|
||||||
"type.googleapis.com/kiapi.board.types.Via"
|
"type.googleapis.com/kiapi.board.types.Via"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selection_item_detail_reports_track_fields() {
|
||||||
|
let track = crate::proto::kiapi::board::types::Track {
|
||||||
|
id: Some(crate::proto::kiapi::common::types::Kiid {
|
||||||
|
value: "track-id".to_string(),
|
||||||
|
}),
|
||||||
|
start: Some(crate::proto::kiapi::common::types::Vector2 { x_nm: 1, y_nm: 2 }),
|
||||||
|
end: Some(crate::proto::kiapi::common::types::Vector2 { x_nm: 3, y_nm: 4 }),
|
||||||
|
width: Some(crate::proto::kiapi::common::types::Distance { value_nm: 99 }),
|
||||||
|
locked: 0,
|
||||||
|
layer: crate::proto::kiapi::board::types::BoardLayer::BlFCu as i32,
|
||||||
|
net: Some(crate::proto::kiapi::board::types::Net {
|
||||||
|
code: Some(crate::proto::kiapi::board::types::NetCode { value: 12 }),
|
||||||
|
name: "GND".to_string(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let item = prost_types::Any {
|
||||||
|
type_url: super::envelope::type_url("kiapi.board.types.Track"),
|
||||||
|
value: track.encode_to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let detail = selection_item_detail(&item).expect("track detail should decode");
|
||||||
|
assert!(detail.contains("track id=track-id"));
|
||||||
|
assert!(detail.contains("layer=BL_F_Cu"));
|
||||||
|
assert!(detail.contains("net=12:GND"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pad_netlist_from_footprint_items_extracts_pad_entries() {
|
||||||
|
let pad = crate::proto::kiapi::board::types::Pad {
|
||||||
|
id: Some(crate::proto::kiapi::common::types::Kiid {
|
||||||
|
value: "pad-id".to_string(),
|
||||||
|
}),
|
||||||
|
locked: 0,
|
||||||
|
number: "1".to_string(),
|
||||||
|
net: Some(crate::proto::kiapi::board::types::Net {
|
||||||
|
code: Some(crate::proto::kiapi::board::types::NetCode { value: 5 }),
|
||||||
|
name: "Net-(P1-PM)".to_string(),
|
||||||
|
}),
|
||||||
|
r#type: crate::proto::kiapi::board::types::PadType::PtPth as i32,
|
||||||
|
pad_stack: None,
|
||||||
|
position: None,
|
||||||
|
copper_clearance_override: None,
|
||||||
|
pad_to_die_length: None,
|
||||||
|
symbol_pin: None,
|
||||||
|
pad_to_die_delay: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let footprint = crate::proto::kiapi::board::types::FootprintInstance {
|
||||||
|
id: Some(crate::proto::kiapi::common::types::Kiid {
|
||||||
|
value: "fp-id".to_string(),
|
||||||
|
}),
|
||||||
|
position: None,
|
||||||
|
orientation: None,
|
||||||
|
layer: crate::proto::kiapi::board::types::BoardLayer::BlFCu as i32,
|
||||||
|
locked: 0,
|
||||||
|
definition: Some(crate::proto::kiapi::board::types::Footprint {
|
||||||
|
id: None,
|
||||||
|
anchor: None,
|
||||||
|
attributes: None,
|
||||||
|
overrides: None,
|
||||||
|
net_ties: Vec::new(),
|
||||||
|
private_layers: Vec::new(),
|
||||||
|
reference_field: None,
|
||||||
|
value_field: None,
|
||||||
|
datasheet_field: None,
|
||||||
|
description_field: None,
|
||||||
|
items: vec![prost_types::Any {
|
||||||
|
type_url: super::envelope::type_url("kiapi.board.types.Pad"),
|
||||||
|
value: pad.encode_to_vec(),
|
||||||
|
}],
|
||||||
|
jumpers: None,
|
||||||
|
}),
|
||||||
|
reference_field: Some(crate::proto::kiapi::board::types::Field {
|
||||||
|
id: None,
|
||||||
|
name: "Reference".to_string(),
|
||||||
|
text: Some(crate::proto::kiapi::board::types::BoardText {
|
||||||
|
id: None,
|
||||||
|
text: Some(crate::proto::kiapi::common::types::Text {
|
||||||
|
position: None,
|
||||||
|
attributes: None,
|
||||||
|
text: "P1".to_string(),
|
||||||
|
hyperlink: String::new(),
|
||||||
|
}),
|
||||||
|
layer: 0,
|
||||||
|
knockout: false,
|
||||||
|
locked: 0,
|
||||||
|
}),
|
||||||
|
visible: true,
|
||||||
|
}),
|
||||||
|
value_field: None,
|
||||||
|
datasheet_field: None,
|
||||||
|
description_field: None,
|
||||||
|
attributes: None,
|
||||||
|
overrides: None,
|
||||||
|
symbol_path: None,
|
||||||
|
symbol_sheet_name: String::new(),
|
||||||
|
symbol_sheet_filename: String::new(),
|
||||||
|
symbol_footprint_filters: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let items = vec![prost_types::Any {
|
||||||
|
type_url: super::envelope::type_url("kiapi.board.types.FootprintInstance"),
|
||||||
|
value: footprint.encode_to_vec(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let netlist = pad_netlist_from_footprint_items(items)
|
||||||
|
.expect("pad netlist should decode from footprint");
|
||||||
|
assert_eq!(netlist.len(), 1);
|
||||||
|
let entry = &netlist[0];
|
||||||
|
assert_eq!(entry.footprint_reference.as_deref(), Some("P1"));
|
||||||
|
assert_eq!(entry.pad_number, "1");
|
||||||
|
assert_eq!(entry.net_code, Some(5));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ pub enum KiCadError {
|
||||||
#[error("API status error `{code}`: {message}")]
|
#[error("API status error `{code}`: {message}")]
|
||||||
ApiStatus { code: String, message: String },
|
ApiStatus { code: String, message: String },
|
||||||
|
|
||||||
|
#[error("item request status error `{code}`")]
|
||||||
|
ItemStatus { code: String },
|
||||||
|
|
||||||
#[error("API response missing payload for `{expected_type_url}`")]
|
#[error("API response missing payload for `{expected_type_url}`")]
|
||||||
MissingPayload { expected_type_url: String },
|
MissingPayload { expected_type_url: String },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ pub(crate) mod proto;
|
||||||
pub use crate::client::{ClientBuilder, KiCadClient};
|
pub use crate::client::{ClientBuilder, KiCadClient};
|
||||||
pub use crate::error::KiCadError;
|
pub use crate::error::KiCadError;
|
||||||
pub use crate::model::board::{
|
pub use crate::model::board::{
|
||||||
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, Vector2Nm,
|
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
||||||
};
|
};
|
||||||
pub use crate::model::common::{
|
pub use crate::model::common::{
|
||||||
DocumentSpecifier, DocumentType, SelectionSummary, SelectionTypeCount, VersionInfo,
|
DocumentSpecifier, DocumentType, SelectionItemDetail, SelectionSummary, SelectionTypeCount,
|
||||||
|
VersionInfo,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,16 @@ pub struct Vector2Nm {
|
||||||
pub y_nm: i64,
|
pub y_nm: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PadNetEntry {
|
||||||
|
pub footprint_reference: Option<String>,
|
||||||
|
pub footprint_id: Option<String>,
|
||||||
|
pub pad_id: Option<String>,
|
||||||
|
pub pad_number: String,
|
||||||
|
pub net_code: Option<i32>,
|
||||||
|
pub net_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
|
||||||
|
|
@ -104,3 +104,10 @@ pub struct SelectionSummary {
|
||||||
pub total_items: usize,
|
pub total_items: usize,
|
||||||
pub type_url_counts: Vec<SelectionTypeCount>,
|
pub type_url_counts: Vec<SelectionTypeCount>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SelectionItemDetail {
|
||||||
|
pub type_url: String,
|
||||||
|
pub detail: String,
|
||||||
|
pub raw_len: usize,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ enum Command {
|
||||||
VisibleLayers,
|
VisibleLayers,
|
||||||
BoardOrigin { kind: BoardOriginKind },
|
BoardOrigin { kind: BoardOriginKind },
|
||||||
SelectionSummary,
|
SelectionSummary,
|
||||||
|
SelectionDetails,
|
||||||
|
SelectionRaw,
|
||||||
|
NetlistPads,
|
||||||
Smoke,
|
Smoke,
|
||||||
Help,
|
Help,
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +169,46 @@ async fn run() -> Result<(), KiCadError> {
|
||||||
println!("type_url={} count={}", entry.type_url, entry.count);
|
println!("type_url={} count={}", entry.type_url, entry.count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Command::SelectionDetails => {
|
||||||
|
let details = client.get_selection_details().await?;
|
||||||
|
println!("selection_total={}", details.len());
|
||||||
|
for (index, item) in details.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
"[{index}] type_url={} raw_len={} detail={}",
|
||||||
|
item.type_url, item.raw_len, item.detail
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::SelectionRaw => {
|
||||||
|
let items = client.get_selection_raw().await?;
|
||||||
|
println!("selection_total={}", items.len());
|
||||||
|
for (index, item) in items.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
"[{index}] type_url={} raw_len={} raw_hex={}",
|
||||||
|
item.type_url,
|
||||||
|
item.value.len(),
|
||||||
|
bytes_to_hex(&item.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::NetlistPads => {
|
||||||
|
let entries = client.get_pad_netlist().await?;
|
||||||
|
println!("pad_net_entries={}", entries.len());
|
||||||
|
for entry in entries {
|
||||||
|
println!(
|
||||||
|
"footprint_ref={} footprint_id={} pad_id={} pad_number={} net_code={} net_name={}",
|
||||||
|
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(|code| code.to_string())
|
||||||
|
.unwrap_or_else(|| "-".to_string()),
|
||||||
|
entry.net_name.as_deref().unwrap_or("-")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Command::Smoke => {
|
Command::Smoke => {
|
||||||
client.ping().await?;
|
client.ping().await?;
|
||||||
let version = client.get_version().await?;
|
let version = client.get_version().await?;
|
||||||
|
|
@ -254,6 +297,9 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> {
|
||||||
Command::BoardOrigin { kind }
|
Command::BoardOrigin { kind }
|
||||||
}
|
}
|
||||||
"selection-summary" => Command::SelectionSummary,
|
"selection-summary" => Command::SelectionSummary,
|
||||||
|
"selection-details" => Command::SelectionDetails,
|
||||||
|
"selection-raw" => Command::SelectionRaw,
|
||||||
|
"netlist-pads" => Command::NetlistPads,
|
||||||
"smoke" => Command::Smoke,
|
"smoke" => Command::Smoke,
|
||||||
"open-docs" => {
|
"open-docs" => {
|
||||||
let mut document_type = DocumentType::Pcb;
|
let mut document_type = DocumentType::Pcb;
|
||||||
|
|
@ -292,6 +338,23 @@ fn default_config() -> CliConfig {
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
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 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 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 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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bytes_to_hex(data: &[u8]) -> String {
|
||||||
|
let mut output = String::with_capacity(data.len() * 2);
|
||||||
|
for byte in data {
|
||||||
|
output.push(hex_char((byte >> 4) & 0x0f));
|
||||||
|
output.push(hex_char(byte & 0x0f));
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_char(value: u8) -> char {
|
||||||
|
match value {
|
||||||
|
0..=9 => char::from(b'0' + value),
|
||||||
|
10..=15 => char::from(b'a' + (value - 10)),
|
||||||
|
_ => '?',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue