feat(board): add UpdateBoardStackup API and CLI command

This commit is contained in:
Milind Sharma 2026-02-20 18:56:32 +08:00
parent 0e8217fd8f
commit deb03b9c48
5 changed files with 262 additions and 13 deletions

View File

@ -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` |

View File

@ -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
``` ```

View File

@ -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 {

View File

@ -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