From 54c0bbf7b7bf4aa65e02614006950bf71c3efbc9 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Wed, 18 Feb 2026 23:34:18 +0800 Subject: [PATCH] feat: add visible layers and board origin APIs --- docs/TEST_CLI.md | 18 ++++++++++ src/client.rs | 47 +++++++++++++++++++++++++- src/lib.rs | 4 ++- src/model/board.rs | 62 +++++++++++++++++++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 41 +++++++++++++++++++++-- 5 files changed, 168 insertions(+), 4 deletions(-) diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index a096117..86474f9 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -59,6 +59,24 @@ Show active layer: cargo run --bin kicad-ipc-cli -- active-layer ``` +Show visible layers: + +```bash +cargo run --bin kicad-ipc-cli -- visible-layers +``` + +Show board origin (grid origin by default): + +```bash +cargo run --bin kicad-ipc-cli -- board-origin +``` + +Show drill origin: + +```bash +cargo run --bin kicad-ipc-cli -- board-origin --type drill +``` + Get current project path (derived from open PCB docs): ```bash diff --git a/src/client.rs b/src/client.rs index 1ec63bd..a3d4044 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,7 +5,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::envelope; use crate::error::KiCadError; -use crate::model::board::{BoardEnabledLayers, BoardLayerInfo, BoardNet}; +use crate::model::board::{ + BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, Vector2Nm, +}; use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo, VersionInfo}; use crate::proto::kiapi::board::commands as board_commands; use crate::proto::kiapi::board::types as board_types; @@ -22,12 +24,16 @@ const CMD_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocuments"; const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets"; const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers"; 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 RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse"; const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse"; 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_VECTOR2: &str = "kiapi.common.types.Vector2"; #[derive(Clone, Debug)] pub struct KiCadClient { @@ -251,6 +257,38 @@ impl KiCadClient { Ok(layer_to_model(payload.layer)) } + pub async fn get_visible_layers(&self) -> Result, KiCadError> { + let board = self.current_board_document_proto().await?; + let command = board_commands::GetVisibleLayers { board: Some(board) }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_GET_VISIBLE_LAYERS)) + .await?; + + let payload: board_commands::BoardLayers = + envelope::unpack_any(&response, RES_BOARD_LAYERS)?; + + Ok(payload.layers.into_iter().map(layer_to_model).collect()) + } + + pub async fn get_board_origin(&self, kind: BoardOriginKind) -> Result { + let board = self.current_board_document_proto().await?; + let command = board_commands::GetBoardOrigin { + board: Some(board), + r#type: board_origin_kind_to_proto(kind), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_GET_BOARD_ORIGIN)) + .await?; + + let payload: common_types::Vector2 = envelope::unpack_any(&response, RES_VECTOR2)?; + Ok(Vector2Nm { + x_nm: payload.x_nm, + y_nm: payload.y_nm, + }) + } + async fn send_command( &self, command: prost_types::Any, @@ -357,6 +395,13 @@ fn layer_to_model(layer_id: i32) -> BoardLayerInfo { BoardLayerInfo { id: layer_id, name } } +fn board_origin_kind_to_proto(kind: BoardOriginKind) -> i32 { + match kind { + BoardOriginKind::Grid => board_commands::BoardOriginType::BotGrid as i32, + BoardOriginKind::Drill => board_commands::BoardOriginType::BotDrill as i32, + } +} + fn select_single_board_document( docs: &[DocumentSpecifier], ) -> Result<&DocumentSpecifier, KiCadError> { diff --git a/src/lib.rs b/src/lib.rs index 37df82a..3747da8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,5 +20,7 @@ pub(crate) mod proto; pub use crate::client::{ClientBuilder, KiCadClient}; pub use crate::error::KiCadError; -pub use crate::model::board::{BoardEnabledLayers, BoardLayerInfo, BoardNet}; +pub use crate::model::board::{ + BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, Vector2Nm, +}; pub use crate::model::common::{DocumentSpecifier, DocumentType, VersionInfo}; diff --git a/src/model/board.rs b/src/model/board.rs index 1afb58d..851417f 100644 --- a/src/model/board.rs +++ b/src/model/board.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct BoardNet { pub code: i32, @@ -15,3 +17,63 @@ pub struct BoardEnabledLayers { pub copper_layer_count: u32, pub layers: Vec, } + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BoardOriginKind { + Grid, + Drill, +} + +impl FromStr for BoardOriginKind { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "grid" => Ok(Self::Grid), + "drill" => Ok(Self::Drill), + _ => Err(format!( + "unknown board origin kind `{value}`; expected `grid` or `drill`" + )), + } + } +} + +impl std::fmt::Display for BoardOriginKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Grid => write!(f, "grid"), + Self::Drill => write!(f, "drill"), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Vector2Nm { + pub x_nm: i64, + pub y_nm: i64, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::BoardOriginKind; + + #[test] + fn board_origin_kind_parses_known_values() { + assert_eq!( + BoardOriginKind::from_str("grid").expect("grid should parse"), + BoardOriginKind::Grid + ); + assert_eq!( + BoardOriginKind::from_str("drill").expect("drill should parse"), + BoardOriginKind::Drill + ); + } + + #[test] + fn board_origin_kind_rejects_unknown_values() { + let result = BoardOriginKind::from_str("other"); + assert!(result.is_err()); + } +} diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 9158db9..d603fd4 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -2,7 +2,7 @@ use std::process::ExitCode; use std::str::FromStr; use std::time::Duration; -use kicad_ipc::{ClientBuilder, DocumentType, KiCadError}; +use kicad_ipc::{BoardOriginKind, ClientBuilder, DocumentType, KiCadError}; #[derive(Debug)] struct CliConfig { @@ -21,6 +21,8 @@ enum Command { Nets, EnabledLayers, ActiveLayer, + VisibleLayers, + BoardOrigin { kind: BoardOriginKind }, Smoke, Help, } @@ -139,6 +141,23 @@ async fn run() -> Result<(), KiCadError> { layer.id, layer.name ); } + Command::VisibleLayers => { + let layers = client.get_visible_layers().await?; + if layers.is_empty() { + println!("no visible layers returned"); + } else { + for layer in layers { + println!("layer_id={} layer_name={}", layer.id, layer.name); + } + } + } + Command::BoardOrigin { kind } => { + let origin = client.get_board_origin(kind).await?; + println!( + "origin_kind={} x_nm={} y_nm={}", + kind, origin.x_nm, origin.y_nm + ); + } Command::Smoke => { client.ping().await?; let version = client.get_version().await?; @@ -208,6 +227,24 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> { "nets" => Command::Nets, "enabled-layers" => Command::EnabledLayers, "active-layer" => Command::ActiveLayer, + "visible-layers" => Command::VisibleLayers, + "board-origin" => { + let mut kind = BoardOriginKind::Grid; + let mut i = 1; + while i < args.len() { + if args[i] == "--type" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for board-origin --type".to_string(), + })?; + kind = BoardOriginKind::from_str(value) + .map_err(|err| KiCadError::Config { reason: err })?; + i += 2; + continue; + } + i += 1; + } + Command::BoardOrigin { kind } + } "smoke" => Command::Smoke, "open-docs" => { let mut document_type = DocumentType::Pcb; @@ -246,6 +283,6 @@ 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 options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--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 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 options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--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 ] Show board origin (`grid` default, or `drill`)\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n" ); }