feat(board): add UpdateBoardStackup API and CLI command
This commit is contained in:
parent
0e8217fd8f
commit
deb03b9c48
|
|
@ -45,6 +45,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un
|
||||||
- `ParseAndCreateItemsFromString`
|
- `ParseAndCreateItemsFromString`
|
||||||
- `SetNetClasses`
|
- `SetNetClasses`
|
||||||
- `SetTextVariables`
|
- `SetTextVariables`
|
||||||
|
- `UpdateBoardStackup`
|
||||||
|
|
||||||
## KiCad v10 RC1.1 API Completion Matrix
|
## KiCad v10 RC1.1 API Completion Matrix
|
||||||
|
|
||||||
|
|
@ -67,9 +68,9 @@ Legend:
|
||||||
| Common (base) | 6 | 6 | 100% |
|
| Common (base) | 6 | 6 | 100% |
|
||||||
| Common editor/document | 23 | 23 | 100% |
|
| Common editor/document | 23 | 23 | 100% |
|
||||||
| Project manager | 5 | 5 | 100% |
|
| Project manager | 5 | 5 | 100% |
|
||||||
| Board editor (PCB) | 22 | 20 | 91% |
|
| Board editor (PCB) | 22 | 21 | 95% |
|
||||||
| Schematic editor (dedicated proto commands) | 0 | 0 | n/a |
|
| Schematic editor (dedicated proto commands) | 0 | 0 | n/a |
|
||||||
| **Total** | **56** | **54** | **96%** |
|
| **Total** | **56** | **55** | **98%** |
|
||||||
|
|
||||||
### Common (base)
|
### Common (base)
|
||||||
|
|
||||||
|
|
@ -125,7 +126,7 @@ Legend:
|
||||||
| KiCad Command | Status | Rust API |
|
| KiCad Command | Status | Rust API |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GetBoardStackup` | Implemented | `KiCadClient::get_board_stackup_raw`, `KiCadClient::get_board_stackup` |
|
| `GetBoardStackup` | Implemented | `KiCadClient::get_board_stackup_raw`, `KiCadClient::get_board_stackup` |
|
||||||
| `UpdateBoardStackup` | Not yet | - |
|
| `UpdateBoardStackup` | Implemented | `KiCadClient::update_board_stackup_raw`, `KiCadClient::update_board_stackup` |
|
||||||
| `GetBoardEnabledLayers` | Implemented | `KiCadClient::get_board_enabled_layers` |
|
| `GetBoardEnabledLayers` | Implemented | `KiCadClient::get_board_enabled_layers` |
|
||||||
| `SetBoardEnabledLayers` | Implemented | `KiCadClient::set_board_enabled_layers` |
|
| `SetBoardEnabledLayers` | Implemented | `KiCadClient::set_board_enabled_layers` |
|
||||||
| `GetGraphicsDefaults` | Implemented | `KiCadClient::get_graphics_defaults_raw`, `KiCadClient::get_graphics_defaults` |
|
| `GetGraphicsDefaults` | Implemented | `KiCadClient::get_graphics_defaults_raw`, `KiCadClient::get_graphics_defaults` |
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,7 @@ Show typed stackup/graphics/appearance:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run --bin kicad-ipc-cli -- stackup
|
cargo run --bin kicad-ipc-cli -- stackup
|
||||||
|
cargo run --bin kicad-ipc-cli -- update-stackup
|
||||||
cargo run --bin kicad-ipc-cli -- graphics-defaults
|
cargo run --bin kicad-ipc-cli -- graphics-defaults
|
||||||
cargo run --bin kicad-ipc-cli -- appearance
|
cargo run --bin kicad-ipc-cli -- appearance
|
||||||
```
|
```
|
||||||
|
|
|
||||||
250
src/client.rs
250
src/client.rs
|
|
@ -59,6 +59,7 @@ const CMD_SET_VISIBLE_LAYERS: &str = "kiapi.board.commands.SetVisibleLayers";
|
||||||
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
||||||
const CMD_SET_BOARD_ORIGIN: &str = "kiapi.board.commands.SetBoardOrigin";
|
const CMD_SET_BOARD_ORIGIN: &str = "kiapi.board.commands.SetBoardOrigin";
|
||||||
const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup";
|
const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup";
|
||||||
|
const CMD_UPDATE_BOARD_STACKUP: &str = "kiapi.board.commands.UpdateBoardStackup";
|
||||||
const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults";
|
const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults";
|
||||||
const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
||||||
"kiapi.board.commands.GetBoardEditorAppearanceSettings";
|
"kiapi.board.commands.GetBoardEditorAppearanceSettings";
|
||||||
|
|
@ -1524,6 +1525,32 @@ impl KiCadClient {
|
||||||
Ok(map_board_stackup(response.stackup.unwrap_or_default()))
|
Ok(map_board_stackup(response.stackup.unwrap_or_default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_board_stackup_raw(
|
||||||
|
&self,
|
||||||
|
stackup: BoardStackup,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = board_commands::UpdateBoardStackup {
|
||||||
|
board: Some(self.current_board_document_proto().await?),
|
||||||
|
stackup: Some(board_stackup_to_proto(stackup)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_UPDATE_BOARD_STACKUP))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_board_stackup(
|
||||||
|
&self,
|
||||||
|
stackup: BoardStackup,
|
||||||
|
) -> Result<BoardStackup, KiCadError> {
|
||||||
|
let payload = self.update_board_stackup_raw(stackup).await?;
|
||||||
|
let response: board_commands::BoardStackupResponse =
|
||||||
|
decode_any(&payload, RES_BOARD_STACKUP_RESPONSE)?;
|
||||||
|
Ok(map_board_stackup(response.stackup.unwrap_or_default()))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_graphics_defaults_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
pub async fn get_graphics_defaults_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
let command = board_commands::GetGraphicsDefaults {
|
let command = board_commands::GetGraphicsDefaults {
|
||||||
board: Some(self.current_board_document_proto().await?),
|
board: Some(self.current_board_document_proto().await?),
|
||||||
|
|
@ -2510,6 +2537,28 @@ fn map_board_stackup_layer_type(value: i32) -> BoardStackupLayerType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn board_stackup_layer_type_to_proto(value: BoardStackupLayerType) -> i32 {
|
||||||
|
match value {
|
||||||
|
BoardStackupLayerType::Copper => board_proto::BoardStackupLayerType::BsltCopper as i32,
|
||||||
|
BoardStackupLayerType::Dielectric => {
|
||||||
|
board_proto::BoardStackupLayerType::BsltDielectric as i32
|
||||||
|
}
|
||||||
|
BoardStackupLayerType::Silkscreen => {
|
||||||
|
board_proto::BoardStackupLayerType::BsltSilkscreen as i32
|
||||||
|
}
|
||||||
|
BoardStackupLayerType::SolderMask => {
|
||||||
|
board_proto::BoardStackupLayerType::BsltSoldermask as i32
|
||||||
|
}
|
||||||
|
BoardStackupLayerType::SolderPaste => {
|
||||||
|
board_proto::BoardStackupLayerType::BsltSolderpaste as i32
|
||||||
|
}
|
||||||
|
BoardStackupLayerType::Undefined => {
|
||||||
|
board_proto::BoardStackupLayerType::BsltUndefined as i32
|
||||||
|
}
|
||||||
|
BoardStackupLayerType::Unknown(value) => value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_board_layer_class(value: i32) -> BoardLayerClass {
|
fn map_board_layer_class(value: i32) -> BoardLayerClass {
|
||||||
match board_proto::BoardLayerClass::try_from(value) {
|
match board_proto::BoardLayerClass::try_from(value) {
|
||||||
Ok(board_proto::BoardLayerClass::BlcSilkscreen) => BoardLayerClass::Silkscreen,
|
Ok(board_proto::BoardLayerClass::BlcSilkscreen) => BoardLayerClass::Silkscreen,
|
||||||
|
|
@ -2616,6 +2665,7 @@ fn map_board_stackup(stackup: board_proto::BoardStackup) -> BoardStackup {
|
||||||
.map(|impedance| impedance.is_controlled)
|
.map(|impedance| impedance.is_controlled)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let edge = stackup.edge.unwrap_or_default();
|
let edge = stackup.edge.unwrap_or_default();
|
||||||
|
let edge_has_connector = edge.connector.is_some();
|
||||||
let edge_has_castellated_pads = edge
|
let edge_has_castellated_pads = edge
|
||||||
.castellation
|
.castellation
|
||||||
.map(|value| value.has_castellated_pads)
|
.map(|value| value.has_castellated_pads)
|
||||||
|
|
@ -2654,12 +2704,75 @@ fn map_board_stackup(stackup: board_proto::BoardStackup) -> BoardStackup {
|
||||||
BoardStackup {
|
BoardStackup {
|
||||||
finish_type_name,
|
finish_type_name,
|
||||||
impedance_controlled,
|
impedance_controlled,
|
||||||
|
edge_has_connector,
|
||||||
edge_has_castellated_pads,
|
edge_has_castellated_pads,
|
||||||
edge_has_edge_plating,
|
edge_has_edge_plating,
|
||||||
layers,
|
layers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn board_stackup_to_proto(stackup: BoardStackup) -> board_proto::BoardStackup {
|
||||||
|
board_proto::BoardStackup {
|
||||||
|
finish: (!stackup.finish_type_name.is_empty()).then_some(board_proto::BoardFinish {
|
||||||
|
type_name: stackup.finish_type_name,
|
||||||
|
}),
|
||||||
|
impedance: Some(board_proto::BoardImpedanceControl {
|
||||||
|
is_controlled: stackup.impedance_controlled,
|
||||||
|
}),
|
||||||
|
edge: Some(board_proto::BoardEdgeSettings {
|
||||||
|
connector: stackup
|
||||||
|
.edge_has_connector
|
||||||
|
.then_some(board_proto::BoardEdgeConnector {}),
|
||||||
|
castellation: Some(board_proto::Castellation {
|
||||||
|
has_castellated_pads: stackup.edge_has_castellated_pads,
|
||||||
|
}),
|
||||||
|
plating: Some(board_proto::EdgePlating {
|
||||||
|
has_edge_plating: stackup.edge_has_edge_plating,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
layers: stackup
|
||||||
|
.layers
|
||||||
|
.into_iter()
|
||||||
|
.map(board_stackup_layer_to_proto)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn board_stackup_layer_to_proto(layer: BoardStackupLayer) -> board_proto::BoardStackupLayer {
|
||||||
|
board_proto::BoardStackupLayer {
|
||||||
|
thickness: layer
|
||||||
|
.thickness_nm
|
||||||
|
.map(|value_nm| common_types::Distance { value_nm }),
|
||||||
|
layer: layer.layer.id,
|
||||||
|
enabled: layer.enabled,
|
||||||
|
r#type: board_stackup_layer_type_to_proto(layer.layer_type),
|
||||||
|
dielectric: (!layer.dielectric_layers.is_empty()).then(|| {
|
||||||
|
board_proto::BoardStackupDielectricLayer {
|
||||||
|
layer: layer
|
||||||
|
.dielectric_layers
|
||||||
|
.into_iter()
|
||||||
|
.map(|dielectric| board_proto::BoardStackupDielectricProperties {
|
||||||
|
epsilon_r: dielectric.epsilon_r,
|
||||||
|
loss_tangent: dielectric.loss_tangent,
|
||||||
|
material_name: dielectric.material_name,
|
||||||
|
thickness: dielectric
|
||||||
|
.thickness_nm
|
||||||
|
.map(|value_nm| common_types::Distance { value_nm }),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
color: layer.color.map(|color| common_types::Color {
|
||||||
|
r: color.r,
|
||||||
|
g: color.g,
|
||||||
|
b: color.b,
|
||||||
|
a: color.a,
|
||||||
|
}),
|
||||||
|
material_name: layer.material_name,
|
||||||
|
user_name: layer.user_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_graphics_defaults(defaults: board_proto::GraphicsDefaults) -> GraphicsDefaults {
|
fn map_graphics_defaults(defaults: board_proto::GraphicsDefaults) -> GraphicsDefaults {
|
||||||
GraphicsDefaults {
|
GraphicsDefaults {
|
||||||
layers: defaults
|
layers: defaults
|
||||||
|
|
@ -3495,17 +3608,20 @@ fn default_client_name() -> String {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto,
|
any_to_pretty_debug, board_editor_appearance_settings_to_proto, board_stackup_to_proto,
|
||||||
drc_severity_to_proto, ensure_item_deletion_status_ok, ensure_item_request_ok,
|
commit_action_to_proto, drc_severity_to_proto, ensure_item_deletion_status_ok,
|
||||||
ensure_item_status_ok, layer_to_model, map_commit_session, map_hit_test_result,
|
ensure_item_request_ok, ensure_item_status_ok, layer_to_model, map_board_stackup,
|
||||||
map_item_bounding_boxes, map_merge_mode_to_proto, map_polygon_with_holes,
|
map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_merge_mode_to_proto,
|
||||||
map_run_action_status, model_document_to_proto, normalize_socket_uri,
|
map_polygon_with_holes, map_run_action_status, model_document_to_proto,
|
||||||
pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document,
|
normalize_socket_uri, pad_netlist_from_footprint_items, response_payload_as_any,
|
||||||
select_single_project_path, selection_item_detail, summarize_item_details,
|
select_single_board_document, select_single_project_path, selection_item_detail,
|
||||||
summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto,
|
summarize_item_details, summarize_selection, text_horizontal_alignment_to_proto,
|
||||||
PCB_OBJECT_TYPES,
|
text_spec_to_proto, PCB_OBJECT_TYPES,
|
||||||
};
|
};
|
||||||
use crate::error::KiCadError;
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::{
|
||||||
|
BoardLayerInfo, BoardStackup, BoardStackupLayer, BoardStackupLayerType,
|
||||||
|
};
|
||||||
use crate::model::common::{
|
use crate::model::common::{
|
||||||
CommitAction, DocumentSpecifier, DocumentType, ProjectInfo, TextAttributesSpec,
|
CommitAction, DocumentSpecifier, DocumentType, ProjectInfo, TextAttributesSpec,
|
||||||
TextHorizontalAlignment, TextSpec,
|
TextHorizontalAlignment, TextSpec,
|
||||||
|
|
@ -3724,6 +3840,122 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_board_stackup_defaults_missing_optional_messages() {
|
||||||
|
let mapped = map_board_stackup(crate::proto::kiapi::board::BoardStackup::default());
|
||||||
|
assert_eq!(mapped.finish_type_name, "");
|
||||||
|
assert!(!mapped.impedance_controlled);
|
||||||
|
assert!(!mapped.edge_has_connector);
|
||||||
|
assert!(!mapped.edge_has_castellated_pads);
|
||||||
|
assert!(!mapped.edge_has_edge_plating);
|
||||||
|
assert!(mapped.layers.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_board_stackup_maps_unknown_layer_type_enum() {
|
||||||
|
let mapped = map_board_stackup(crate::proto::kiapi::board::BoardStackup {
|
||||||
|
finish: None,
|
||||||
|
impedance: None,
|
||||||
|
edge: None,
|
||||||
|
layers: vec![crate::proto::kiapi::board::BoardStackupLayer {
|
||||||
|
thickness: None,
|
||||||
|
layer: crate::proto::kiapi::board::types::BoardLayer::BlFCu as i32,
|
||||||
|
enabled: true,
|
||||||
|
r#type: 777,
|
||||||
|
dielectric: None,
|
||||||
|
color: None,
|
||||||
|
material_name: String::new(),
|
||||||
|
user_name: String::new(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
assert!(matches!(
|
||||||
|
mapped.layers.first().map(|layer| layer.layer_type),
|
||||||
|
Some(BoardStackupLayerType::Unknown(777))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn board_stackup_to_proto_maps_unknown_layer_type_and_missing_nested_messages() {
|
||||||
|
let proto = board_stackup_to_proto(BoardStackup {
|
||||||
|
finish_type_name: String::new(),
|
||||||
|
impedance_controlled: false,
|
||||||
|
edge_has_connector: false,
|
||||||
|
edge_has_castellated_pads: false,
|
||||||
|
edge_has_edge_plating: false,
|
||||||
|
layers: vec![BoardStackupLayer {
|
||||||
|
layer: BoardLayerInfo {
|
||||||
|
id: crate::proto::kiapi::board::types::BoardLayer::BlFCu as i32,
|
||||||
|
name: "BL_F_Cu".to_string(),
|
||||||
|
},
|
||||||
|
user_name: "F.Cu".to_string(),
|
||||||
|
material_name: "Copper".to_string(),
|
||||||
|
enabled: true,
|
||||||
|
thickness_nm: None,
|
||||||
|
layer_type: BoardStackupLayerType::Unknown(321),
|
||||||
|
color: None,
|
||||||
|
dielectric_layers: Vec::new(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(proto.finish.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
proto
|
||||||
|
.impedance
|
||||||
|
.expect("impedance should always be present")
|
||||||
|
.is_controlled,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
let edge = proto.edge.expect("edge should always be present");
|
||||||
|
assert!(edge.connector.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
edge.castellation
|
||||||
|
.expect("castellation should be present")
|
||||||
|
.has_castellated_pads,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
edge.plating
|
||||||
|
.expect("plating should be present")
|
||||||
|
.has_edge_plating,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
let layer = proto.layers.first().expect("one layer should be present");
|
||||||
|
assert!(layer.thickness.is_none());
|
||||||
|
assert_eq!(layer.r#type, 321);
|
||||||
|
assert!(layer.dielectric.is_none());
|
||||||
|
assert!(layer.color.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn board_stackup_to_proto_preserves_edge_connector_presence() {
|
||||||
|
let proto = board_stackup_to_proto(BoardStackup {
|
||||||
|
finish_type_name: "ENIG".to_string(),
|
||||||
|
impedance_controlled: true,
|
||||||
|
edge_has_connector: true,
|
||||||
|
edge_has_castellated_pads: true,
|
||||||
|
edge_has_edge_plating: true,
|
||||||
|
layers: Vec::new(),
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
proto.finish.expect("finish should be present").type_name,
|
||||||
|
"ENIG"
|
||||||
|
);
|
||||||
|
let edge = proto.edge.expect("edge should be present");
|
||||||
|
assert!(edge.connector.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
edge.castellation
|
||||||
|
.expect("castellation should be present")
|
||||||
|
.has_castellated_pads,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
edge.plating
|
||||||
|
.expect("plating should be present")
|
||||||
|
.has_edge_plating,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn response_payload_as_any_validates_type_url() {
|
fn response_payload_as_any_validates_type_url() {
|
||||||
let response = crate::proto::kiapi::common::ApiResponse {
|
let response = crate::proto::kiapi::common::ApiResponse {
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ pub struct BoardStackupLayer {
|
||||||
pub struct BoardStackup {
|
pub struct BoardStackup {
|
||||||
pub finish_type_name: String,
|
pub finish_type_name: String,
|
||||||
pub impedance_controlled: bool,
|
pub impedance_controlled: bool,
|
||||||
|
pub edge_has_connector: bool,
|
||||||
pub edge_has_castellated_pads: bool,
|
pub edge_has_castellated_pads: bool,
|
||||||
pub edge_has_edge_plating: bool,
|
pub edge_has_edge_plating: bool,
|
||||||
pub layers: Vec<BoardStackupLayer>,
|
pub layers: Vec<BoardStackupLayer>,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue