diff --git a/Cargo.toml b/Cargo.toml index 95b60b1..41019df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ keywords = ["kicad", "eda", "pcb", "ipc"] categories = ["api-bindings", "asynchronous"] include = [ "/src/**", + "/test-scripts/kicad-ipc-cli.rs", + "/docs/TEST_CLI.md", "/README.md", "/LICENSE", "/Cargo.toml", @@ -22,6 +24,11 @@ async = ["dep:nng", "dep:prost", "dep:prost-types", "dep:tokio"] blocking = ["async"] tracing = ["dep:tracing"] +[[bin]] +name = "kicad-ipc-cli" +path = "test-scripts/kicad-ipc-cli.rs" +required-features = ["blocking"] + [dependencies] nng = { version = "1.0.1", optional = true } prost = { version = "0.14.3", optional = true } diff --git a/README.md b/README.md index 04884f6..3b52bbd 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,66 @@ Maintainer workflow: see `CONTRIBUTIONS.md`. ## Status -Alpha. `v0.1.0` release candidate. +Alpha. `v0.1.1` released. -- Async API: implemented and usable. -- Sync/blocking wrapper API: planned, not shipped yet. +- Async API (default): implemented and usable. +- Sync/blocking wrapper API (`feature = "blocking"`): implemented with full async parity. - Real-world user testing: still limited. - Issues and PRs welcome. +## Usage + +### Async API (Default) + +`Cargo.toml`: + +```toml +[dependencies] +kicad-ipc-rs = "0.1.1" +tokio = { version = "1", features = ["macros", "rt"] } +``` + +```rust +use kicad_ipc_rs::KiCadClient; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), kicad_ipc_rs::KiCadError> { + let client = KiCadClient::connect().await?; + client.ping().await?; + let version = client.get_version().await?; + println!("KiCad: {}", version.full_version); + Ok(()) +} +``` + +### Sync API (Blocking) + +Enable the `blocking` feature and use `KiCadClientBlocking` for synchronous callers: + +`Cargo.toml`: + +```toml +[dependencies] +kicad-ipc-rs = { version = "0.1.1", features = ["blocking"] } +``` + +```rust +use kicad_ipc_rs::KiCadClientBlocking; + +fn main() -> Result<(), kicad_ipc_rs::KiCadError> { + let client = KiCadClientBlocking::builder().connect()?; + client.ping()?; + let version = client.get_version()?; + println!("KiCad: {}", version.full_version); + Ok(()) +} +``` + +Implementation notes: +- Blocking calls run through a dedicated Tokio runtime thread. +- Requests are serialized through a bounded queue. +- Runtime teardown is graceful: in-flight work drains before worker exit. + ## Protobuf Source This crate ships checked-in Rust protobuf output under `src/proto/generated/`. @@ -34,7 +87,8 @@ The regeneration tool also stamps `KICAD_API_VERSION` from the KiCad submodule g ## Local Testing -- CLI runbook: `/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rust/docs/TEST_CLI.md` +- CLI runbook: `/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/docs/TEST_CLI.md` +- CLI help: `cargo run --features blocking --bin kicad-ipc-cli -- help` ## Runtime Compatibility Notes @@ -169,7 +223,6 @@ Legend: ## Roadmap `v0.2.0` target: -- Add full sync/blocking wrapper API parity over async client. - Expand runtime + integration testing coverage. - Set up CI to run checks/tests on commits and PRs. - Continue API hardening/docs/examples for stable `1.0` path. diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index ab0fe02..e48e067 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -6,9 +6,11 @@ CLI binary path: Run help: ```bash -cargo run --bin kicad-ipc-cli -- help +cargo run --features blocking --bin kicad-ipc-cli -- help ``` +The CLI uses `KiCadClientBlocking` and validates the sync wrapper end-to-end. + ## Prereqs 1. KiCad running. @@ -20,145 +22,145 @@ cargo run --bin kicad-ipc-cli -- help Ping: ```bash -cargo run --bin kicad-ipc-cli -- ping +cargo run --features blocking --bin kicad-ipc-cli -- ping ``` Version: ```bash -cargo run --bin kicad-ipc-cli -- version +cargo run --features blocking --bin kicad-ipc-cli -- version ``` Resolve KiCad binary path (default `kicad-cli`): ```bash -cargo run --bin kicad-ipc-cli -- kicad-binary-path --binary-name kicad-cli +cargo run --features blocking --bin kicad-ipc-cli -- kicad-binary-path --binary-name kicad-cli ``` Resolve plugin settings path (default identifier `kicad-ipc-rust`): ```bash -cargo run --bin kicad-ipc-cli -- plugin-settings-path --identifier kicad-ipc-rust +cargo run --features blocking --bin kicad-ipc-cli -- plugin-settings-path --identifier kicad-ipc-rust ``` List open PCB docs: ```bash -cargo run --bin kicad-ipc-cli -- open-docs --type pcb +cargo run --features blocking --bin kicad-ipc-cli -- open-docs --type pcb ``` Check board open: ```bash -cargo run --bin kicad-ipc-cli -- board-open +cargo run --features blocking --bin kicad-ipc-cli -- board-open ``` List nets: ```bash -cargo run --bin kicad-ipc-cli -- nets +cargo run --features blocking --bin kicad-ipc-cli -- nets ``` List project net classes: ```bash -cargo run --bin kicad-ipc-cli -- net-classes +cargo run --features blocking --bin kicad-ipc-cli -- net-classes ``` Write current net classes back with selected merge mode: ```bash -cargo run --bin kicad-ipc-cli -- set-net-classes --merge-mode merge +cargo run --features blocking --bin kicad-ipc-cli -- set-net-classes --merge-mode merge ``` List text variables for current board document: ```bash -cargo run --bin kicad-ipc-cli -- text-variables +cargo run --features blocking --bin kicad-ipc-cli -- text-variables ``` Set text variables: ```bash -cargo run --bin kicad-ipc-cli -- set-text-variables --merge-mode merge --var REV=A +cargo run --features blocking --bin kicad-ipc-cli -- set-text-variables --merge-mode merge --var REV=A ``` Expand text variables in one or more input strings: ```bash -cargo run --bin kicad-ipc-cli -- expand-text-variables --text "${TITLE}" --text "${REVISION}" +cargo run --features blocking --bin kicad-ipc-cli -- expand-text-variables --text "${TITLE}" --text "${REVISION}" ``` Measure text extents: ```bash -cargo run --bin kicad-ipc-cli -- text-extents --text "R1" +cargo run --features blocking --bin kicad-ipc-cli -- text-extents --text "R1" ``` Convert text to shape primitives: ```bash -cargo run --bin kicad-ipc-cli -- text-as-shapes --text "R1" --text "C5" +cargo run --features blocking --bin kicad-ipc-cli -- text-as-shapes --text "R1" --text "C5" ``` List enabled board layers: ```bash -cargo run --bin kicad-ipc-cli -- enabled-layers +cargo run --features blocking --bin kicad-ipc-cli -- enabled-layers ``` Set enabled board layers: ```bash -cargo run --bin kicad-ipc-cli -- set-enabled-layers --copper-layer-count 2 --layer-id 47 --layer-id 52 +cargo run --features blocking --bin kicad-ipc-cli -- set-enabled-layers --copper-layer-count 2 --layer-id 47 --layer-id 52 ``` Show active layer: ```bash -cargo run --bin kicad-ipc-cli -- active-layer +cargo run --features blocking --bin kicad-ipc-cli -- active-layer ``` Set active layer: ```bash -cargo run --bin kicad-ipc-cli -- set-active-layer --layer-id 0 +cargo run --features blocking --bin kicad-ipc-cli -- set-active-layer --layer-id 0 ``` Show visible layers: ```bash -cargo run --bin kicad-ipc-cli -- visible-layers +cargo run --features blocking --bin kicad-ipc-cli -- visible-layers ``` Set visible layers: ```bash -cargo run --bin kicad-ipc-cli -- set-visible-layers --layer-id 0 --layer-id 31 +cargo run --features blocking --bin kicad-ipc-cli -- set-visible-layers --layer-id 0 --layer-id 31 ``` Show board origin (grid origin by default): ```bash -cargo run --bin kicad-ipc-cli -- board-origin +cargo run --features blocking --bin kicad-ipc-cli -- board-origin ``` Show drill origin: ```bash -cargo run --bin kicad-ipc-cli -- board-origin --type drill +cargo run --features blocking --bin kicad-ipc-cli -- board-origin --type drill ``` Set board origin: ```bash -cargo run --bin kicad-ipc-cli -- set-board-origin --type grid --x-nm 1000000 --y-nm 2000000 +cargo run --features blocking --bin kicad-ipc-cli -- set-board-origin --type grid --x-nm 1000000 --y-nm 2000000 ``` Refresh PCB editor: ```bash -cargo run --bin kicad-ipc-cli -- refresh-editor --frame pcb +cargo run --features blocking --bin kicad-ipc-cli -- refresh-editor --frame pcb ``` If your KiCad build does not expose this handler yet, this call may return `AS_UNHANDLED`. @@ -166,226 +168,226 @@ If your KiCad build does not expose this handler yet, this call may return `AS_U Start a staged commit and print commit ID: ```bash -cargo run --bin kicad-ipc-cli -- --client-name write-test begin-commit +cargo run --features blocking --bin kicad-ipc-cli -- --client-name write-test begin-commit ``` End a staged commit: ```bash -cargo run --bin kicad-ipc-cli -- --client-name write-test end-commit --id --action drop --message "cli test cleanup" +cargo run --features blocking --bin kicad-ipc-cli -- --client-name write-test end-commit --id --action drop --message "cli test cleanup" ``` Save current board document: ```bash -cargo run --bin kicad-ipc-cli -- save-doc +cargo run --features blocking --bin kicad-ipc-cli -- save-doc ``` Save a copy of current board document: ```bash -cargo run --bin kicad-ipc-cli -- save-copy --path /tmp/example.kicad_pcb --overwrite --include-project +cargo run --features blocking --bin kicad-ipc-cli -- save-copy --path /tmp/example.kicad_pcb --overwrite --include-project ``` Revert current board document from disk: ```bash -cargo run --bin kicad-ipc-cli -- revert-doc +cargo run --features blocking --bin kicad-ipc-cli -- revert-doc ``` Run a raw KiCad tool action: ```bash -cargo run --bin kicad-ipc-cli -- run-action --action pcbnew.InteractiveSelection.ClearSelection +cargo run --features blocking --bin kicad-ipc-cli -- run-action --action pcbnew.InteractiveSelection.ClearSelection ``` Create raw Any item payload(s): ```bash -cargo run --bin kicad-ipc-cli -- create-items --item type.googleapis.com/kiapi.board.types.Text= +cargo run --features blocking --bin kicad-ipc-cli -- create-items --item type.googleapis.com/kiapi.board.types.Text= ``` Update raw Any item payload(s): ```bash -cargo run --bin kicad-ipc-cli -- update-items --item type.googleapis.com/kiapi.board.types.Text= +cargo run --features blocking --bin kicad-ipc-cli -- update-items --item type.googleapis.com/kiapi.board.types.Text= ``` Delete items by ID: ```bash -cargo run --bin kicad-ipc-cli -- delete-items --id --id +cargo run --features blocking --bin kicad-ipc-cli -- delete-items --id --id ``` Parse and create items from s-expression: ```bash -cargo run --bin kicad-ipc-cli -- parse-create-items --contents "(kicad_pcb (version 20240108))" +cargo run --features blocking --bin kicad-ipc-cli -- parse-create-items --contents "(kicad_pcb (version 20240108))" ``` Show summary of current PCB selection by item type: ```bash -cargo run --bin kicad-ipc-cli -- selection-summary +cargo run --features blocking --bin kicad-ipc-cli -- selection-summary ``` Show parsed details for currently selected items: ```bash -cargo run --bin kicad-ipc-cli -- selection-details +cargo run --features blocking --bin kicad-ipc-cli -- selection-details ``` Show raw protobuf payload bytes for selected items: ```bash -cargo run --bin kicad-ipc-cli -- selection-raw +cargo run --features blocking --bin kicad-ipc-cli -- selection-raw ``` Add items to current selection: ```bash -cargo run --bin kicad-ipc-cli -- add-to-selection --id --id +cargo run --features blocking --bin kicad-ipc-cli -- add-to-selection --id --id ``` Remove items from current selection: ```bash -cargo run --bin kicad-ipc-cli -- remove-from-selection --id --id +cargo run --features blocking --bin kicad-ipc-cli -- remove-from-selection --id --id ``` Clear current selection: ```bash -cargo run --bin kicad-ipc-cli -- clear-selection +cargo run --features blocking --bin kicad-ipc-cli -- clear-selection ``` Show pad-level netlist entries (footprint/pad/net): ```bash -cargo run --bin kicad-ipc-cli -- netlist-pads +cargo run --features blocking --bin kicad-ipc-cli -- netlist-pads ``` Show parsed details for specific item IDs: ```bash -cargo run --bin kicad-ipc-cli -- items-by-id --id --id +cargo run --features blocking --bin kicad-ipc-cli -- items-by-id --id --id ``` Show item bounding boxes: ```bash -cargo run --bin kicad-ipc-cli -- item-bbox --id +cargo run --features blocking --bin kicad-ipc-cli -- item-bbox --id ``` Include child text in the bounding box (for items such as footprints): ```bash -cargo run --bin kicad-ipc-cli -- item-bbox --id --include-text +cargo run --features blocking --bin kicad-ipc-cli -- item-bbox --id --include-text ``` Run hit-test on a specific item: ```bash -cargo run --bin kicad-ipc-cli -- hit-test --id --x-nm --y-nm --tolerance-nm 0 +cargo run --features blocking --bin kicad-ipc-cli -- hit-test --id --x-nm --y-nm --tolerance-nm 0 ``` List all PCB object type IDs from the proto enum: ```bash -cargo run --bin kicad-ipc-cli -- types-pcb +cargo run --features blocking --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 +cargo run --features blocking --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 +cargo run --features blocking --bin kicad-ipc-cli -- items-raw-all-pcb --debug ``` Check whether pads/vias have flashed padstack shapes on specific layers: ```bash -cargo run --bin kicad-ipc-cli -- padstack-presence --item-id --layer-id 3 --layer-id 34 --debug +cargo run --features blocking --bin kicad-ipc-cli -- padstack-presence --item-id --layer-id 3 --layer-id 34 --debug ``` Get polygonized pad shape(s) on a specific layer: ```bash -cargo run --bin kicad-ipc-cli -- pad-shape-polygon --pad-id --layer-id 3 --debug +cargo run --features blocking --bin kicad-ipc-cli -- pad-shape-polygon --pad-id --layer-id 3 --debug ``` Dump board text (KiCad s-expression): ```bash -cargo run --bin kicad-ipc-cli -- board-as-string +cargo run --features blocking --bin kicad-ipc-cli -- board-as-string ``` Dump selection text (KiCad s-expression): ```bash -cargo run --bin kicad-ipc-cli -- selection-as-string +cargo run --features blocking --bin kicad-ipc-cli -- selection-as-string ``` Dump title block fields: ```bash -cargo run --bin kicad-ipc-cli -- title-block +cargo run --features blocking --bin kicad-ipc-cli -- title-block ``` Show typed stackup/graphics/appearance: ```bash -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 -- appearance +cargo run --features blocking --bin kicad-ipc-cli -- stackup +cargo run --features blocking --bin kicad-ipc-cli -- update-stackup +cargo run --features blocking --bin kicad-ipc-cli -- graphics-defaults +cargo run --features blocking --bin kicad-ipc-cli -- appearance ``` Set editor appearance: ```bash -cargo run --bin kicad-ipc-cli -- set-appearance --inactive-layer-display hidden --net-color-display all --board-flip normal --ratsnest-display all-layers +cargo run --features blocking --bin kicad-ipc-cli -- set-appearance --inactive-layer-display hidden --net-color-display all --board-flip normal --ratsnest-display all-layers ``` Inject DRC marker: ```bash -cargo run --bin kicad-ipc-cli -- inject-drc-error --severity error --message "API marker test" --x-nm 1000000 --y-nm 1000000 +cargo run --features blocking --bin kicad-ipc-cli -- inject-drc-error --severity error --message "API marker test" --x-nm 1000000 --y-nm 1000000 ``` Refill all zones: ```bash -cargo run --bin kicad-ipc-cli -- refill-zones +cargo run --features blocking --bin kicad-ipc-cli -- refill-zones ``` Start interactive move tool for one or more item IDs: ```bash -cargo run --bin kicad-ipc-cli -- interactive-move --id --id +cargo run --features blocking --bin kicad-ipc-cli -- interactive-move --id --id ``` Show typed netclass map: ```bash -cargo run --bin kicad-ipc-cli -- netclass +cargo run --features blocking --bin kicad-ipc-cli -- netclass ``` Print proto command coverage status (board read): ```bash -cargo run --bin kicad-ipc-cli -- proto-coverage-board-read +cargo run --features blocking --bin kicad-ipc-cli -- proto-coverage-board-read ``` Generate full board-read reconstruction markdown report: ```bash -cargo run --bin kicad-ipc-cli -- --timeout-ms 60000 board-read-report --out docs/BOARD_READ_REPORT.md +cargo run --features blocking --bin kicad-ipc-cli -- --timeout-ms 60000 board-read-report --out docs/BOARD_READ_REPORT.md ``` Notes: @@ -395,13 +397,13 @@ Notes: Get current project path (derived from open PCB docs): ```bash -cargo run --bin kicad-ipc-cli -- project-path +cargo run --features blocking --bin kicad-ipc-cli -- project-path ``` Smoke check: ```bash -cargo run --bin kicad-ipc-cli -- smoke +cargo run --features blocking --bin kicad-ipc-cli -- smoke ``` ## Common Flags @@ -409,25 +411,25 @@ cargo run --bin kicad-ipc-cli -- smoke Custom socket: ```bash -cargo run --bin kicad-ipc-cli -- --socket ipc:///tmp/kicad/api.sock ping +cargo run --features blocking --bin kicad-ipc-cli -- --socket ipc:///tmp/kicad/api.sock ping ``` Custom token: ```bash -cargo run --bin kicad-ipc-cli -- --token "$KICAD_API_TOKEN" version +cargo run --features blocking --bin kicad-ipc-cli -- --token "$KICAD_API_TOKEN" version ``` Stable client name (needed when pairing `begin-commit` and `end-commit` across separate CLI runs): ```bash -cargo run --bin kicad-ipc-cli -- --client-name write-test begin-commit +cargo run --features blocking --bin kicad-ipc-cli -- --client-name write-test begin-commit ``` Custom timeout: ```bash -cargo run --bin kicad-ipc-cli -- --timeout-ms 5000 ping +cargo run --features blocking --bin kicad-ipc-cli -- --timeout-ms 5000 ping ``` ## Failure Hints diff --git a/src/blocking.rs b/src/blocking.rs index 033c4f1..31f78c7 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -1,22 +1,664 @@ -use crate::client::KiCadClient; +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::sync::mpsc::{self, SyncSender}; +use std::sync::{Arc, Mutex}; +use std::thread::{self, JoinHandle, ThreadId}; +use std::time::Duration; + +use prost_types::Any; + +use crate::client::{ClientBuilder, KiCadClient}; use crate::error::KiCadError; +use crate::model::board::*; +use crate::model::common::*; + +const BLOCKING_QUEUE_CAPACITY: usize = 64; + +type Job = Box; + +#[derive(Debug)] +struct BlockingCore { + job_tx: Mutex>>, + worker_thread_id: ThreadId, + worker_join: Mutex>>, +} + +impl BlockingCore { + fn start() -> Result, KiCadError> { + let (job_tx, job_rx) = mpsc::sync_channel::(BLOCKING_QUEUE_CAPACITY); + let (init_tx, init_rx) = mpsc::sync_channel::>(1); + + let worker_name = format!("kicad-ipc-blocking-runtime-{}", std::process::id()); + let worker_join = thread::Builder::new() + .name(worker_name) + .spawn(move || { + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + { + Ok(runtime) => runtime, + Err(err) => { + let _ = init_tx.send(Err(KiCadError::RuntimeJoin(err.to_string()))); + return; + } + }; + + let _ = init_tx.send(Ok(thread::current().id())); + + for job in job_rx { + job(&runtime); + } + }) + .map_err(|err| KiCadError::RuntimeJoin(err.to_string()))?; + + let worker_thread_id = init_rx + .recv() + .map_err(|_| KiCadError::BlockingRuntimeClosed)??; + + Ok(Arc::new(Self { + job_tx: Mutex::new(Some(job_tx)), + worker_thread_id, + worker_join: Mutex::new(Some(worker_join)), + })) + } + + fn shutdown(&self) { + if let Ok(mut tx_guard) = self.job_tx.lock() { + tx_guard.take(); + } + + let handle = match self.worker_join.lock() { + Ok(mut guard) => guard.take(), + Err(_) => None, + }; + + if let Some(handle) = handle { + if thread::current().id() != self.worker_thread_id { + let _ = handle.join(); + } + } + } + + fn call(&self, f: F) -> Result + where + T: Send + 'static, + F: FnOnce(&tokio::runtime::Runtime) -> Result + Send + 'static, + { + let sender = { + let guard = self + .job_tx + .lock() + .map_err(|_| KiCadError::BlockingRuntimeClosed)?; + guard + .as_ref() + .cloned() + .ok_or(KiCadError::BlockingRuntimeClosed)? + }; + + let (result_tx, result_rx) = mpsc::sync_channel::>(1); + + sender + .send(Box::new(move |runtime| { + let result = f(runtime); + let _ = result_tx.send(result); + })) + .map_err(|_| KiCadError::BlockingRuntimeClosed)?; + + result_rx + .recv() + .map_err(|_| KiCadError::BlockingRuntimeClosed)? + } +} + +impl Drop for BlockingCore { + fn drop(&mut self) { + self.shutdown(); + } +} #[derive(Clone, Debug)] pub struct KiCadClientBlocking { inner: KiCadClient, + core: Arc, +} + +#[derive(Clone, Debug)] +pub struct KiCadClientBlockingBuilder { + inner: ClientBuilder, +} + +impl KiCadClientBlockingBuilder { + pub fn new() -> Self { + Self { + inner: ClientBuilder::new(), + } + } + + pub fn timeout(mut self, timeout: Duration) -> Self { + self.inner = self.inner.timeout(timeout); + self + } + + pub fn socket_path(mut self, socket_path: impl Into) -> Self { + self.inner = self.inner.socket_path(socket_path); + self + } + + pub fn token(mut self, token: impl Into) -> Self { + self.inner = self.inner.token(token); + self + } + + pub fn client_name(mut self, client_name: impl Into) -> Self { + self.inner = self.inner.client_name(client_name); + self + } + + pub fn connect(self) -> Result { + let core = BlockingCore::start()?; + let inner_builder = self.inner; + let inner = core.call(move |runtime| runtime.block_on(inner_builder.connect()))?; + + Ok(KiCadClientBlocking { inner, core }) + } +} + +impl Default for KiCadClientBlockingBuilder { + fn default() -> Self { + Self::new() + } +} + +macro_rules! blocking_methods { + ( + $(fn $name:ident(&self $(, $arg:ident : $arg_ty:ty)*) -> $ret:ty;)+ + ) => { + $( + pub fn $name(&self, $($arg: $arg_ty),*) -> $ret { + let client = self.inner.clone(); + self.core.call(move |runtime| runtime.block_on(async move { + client.$name($($arg),*).await + })) + } + )+ + + #[cfg(test)] + pub(crate) const GENERATED_BLOCKING_METHOD_NAMES: &'static [&'static str] = &[ + $(stringify!($name),)+ + ]; + }; } impl KiCadClientBlocking { + pub fn builder() -> KiCadClientBlockingBuilder { + KiCadClientBlockingBuilder::new() + } + pub fn connect() -> Result { - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_time() - .build() - .map_err(|err| KiCadError::RuntimeJoin(err.to_string()))?; - let inner = runtime.block_on(KiCadClient::connect())?; - Ok(Self { inner }) + KiCadClientBlockingBuilder::new().connect() + } + + pub fn timeout(&self) -> Duration { + self.inner.timeout() + } + + pub fn socket_uri(&self) -> &str { + self.inner.socket_uri() } pub fn inner(&self) -> &KiCadClient { &self.inner } + + pub fn run_action_raw(&self, action: impl Into) -> Result { + let action = action.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { client.run_action_raw(action).await }) + }) + } + + pub fn run_action(&self, action: impl Into) -> Result { + let action = action.into(); + let client = self.inner.clone(); + self.core + .call(move |runtime| runtime.block_on(async move { client.run_action(action).await })) + } + + pub fn get_kicad_binary_path_raw( + &self, + binary_name: impl Into, + ) -> Result { + let binary_name = binary_name.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { client.get_kicad_binary_path_raw(binary_name).await }) + }) + } + + pub fn get_kicad_binary_path( + &self, + binary_name: impl Into, + ) -> Result { + let binary_name = binary_name.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { client.get_kicad_binary_path(binary_name).await }) + }) + } + + pub fn get_plugin_settings_path_raw( + &self, + identifier: impl Into, + ) -> Result { + let identifier = identifier.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { client.get_plugin_settings_path_raw(identifier).await }) + }) + } + + pub fn get_plugin_settings_path( + &self, + identifier: impl Into, + ) -> Result { + let identifier = identifier.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { client.get_plugin_settings_path(identifier).await }) + }) + } + + pub fn end_commit_raw( + &self, + session: CommitSession, + action: CommitAction, + message: impl Into, + ) -> Result { + let message = message.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { client.end_commit_raw(session, action, message).await }) + }) + } + + pub fn end_commit( + &self, + session: CommitSession, + action: CommitAction, + message: impl Into, + ) -> Result<(), KiCadError> { + let message = message.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { client.end_commit(session, action, message).await }) + }) + } + + pub fn parse_and_create_items_from_string_raw( + &self, + contents: impl Into, + ) -> Result { + let contents = contents.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { + client + .parse_and_create_items_from_string_raw(contents) + .await + }) + }) + } + + pub fn parse_and_create_items_from_string( + &self, + contents: impl Into, + ) -> Result, KiCadError> { + let contents = contents.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime + .block_on(async move { client.parse_and_create_items_from_string(contents).await }) + }) + } + + pub fn inject_drc_error_raw( + &self, + severity: DrcSeverity, + message: impl Into, + position: Option, + item_ids: Vec, + ) -> Result { + let message = message.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { + client + .inject_drc_error_raw(severity, message, position, item_ids) + .await + }) + }) + } + + pub fn inject_drc_error( + &self, + severity: DrcSeverity, + message: impl Into, + position: Option, + item_ids: Vec, + ) -> Result, KiCadError> { + let message = message.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { + client + .inject_drc_error(severity, message, position, item_ids) + .await + }) + }) + } + + pub fn save_copy_of_document_raw( + &self, + path: impl Into, + overwrite: bool, + include_project: bool, + ) -> Result { + let path = path.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { + client + .save_copy_of_document_raw(path, overwrite, include_project) + .await + }) + }) + } + + pub fn save_copy_of_document( + &self, + path: impl Into, + overwrite: bool, + include_project: bool, + ) -> Result<(), KiCadError> { + let path = path.into(); + let client = self.inner.clone(); + self.core.call(move |runtime| { + runtime.block_on(async move { + client + .save_copy_of_document(path, overwrite, include_project) + .await + }) + }) + } + + blocking_methods! { + fn ping(&self) -> Result<(), KiCadError>; + fn refresh_editor(&self, frame: EditorFrameType) -> Result<(), KiCadError>; + fn get_version(&self) -> Result; + fn get_open_documents(&self, document_type: DocumentType) -> Result, KiCadError>; + fn get_net_classes_raw(&self) -> Result; + fn get_net_classes(&self) -> Result, KiCadError>; + fn set_net_classes_raw(&self, net_classes: Vec, merge_mode: MapMergeMode) -> Result; + fn set_net_classes(&self, net_classes: Vec, merge_mode: MapMergeMode) -> Result, KiCadError>; + fn get_text_variables_raw(&self) -> Result; + fn get_text_variables(&self) -> Result, KiCadError>; + fn set_text_variables_raw(&self, variables: BTreeMap, merge_mode: MapMergeMode) -> Result; + fn set_text_variables(&self, variables: BTreeMap, merge_mode: MapMergeMode) -> Result, KiCadError>; + fn expand_text_variables_raw(&self, text: Vec) -> Result; + fn expand_text_variables(&self, text: Vec) -> Result, KiCadError>; + fn get_text_extents_raw(&self, text: TextSpec) -> Result; + fn get_text_extents(&self, text: TextSpec) -> Result; + fn get_text_as_shapes_raw(&self, text: Vec) -> Result; + fn get_text_as_shapes(&self, text: Vec) -> Result, KiCadError>; + fn get_current_project_path(&self) -> Result; + fn has_open_board(&self) -> Result; + fn begin_commit_raw(&self) -> Result; + fn begin_commit(&self) -> Result; + fn create_items_raw(&self, items: Vec, container_id: Option) -> Result; + fn create_items(&self, items: Vec, container_id: Option) -> Result, KiCadError>; + fn update_items_raw(&self, items: Vec) -> Result; + fn update_items(&self, items: Vec) -> Result, KiCadError>; + fn delete_items_raw(&self, item_ids: Vec) -> Result; + fn delete_items(&self, item_ids: Vec) -> Result, KiCadError>; + fn get_nets(&self) -> Result, KiCadError>; + fn get_board_enabled_layers(&self) -> Result; + fn set_board_enabled_layers(&self, copper_layer_count: u32, layer_ids: Vec) -> Result; + fn get_active_layer(&self) -> Result; + fn set_active_layer(&self, layer_id: i32) -> Result<(), KiCadError>; + fn get_visible_layers(&self) -> Result, KiCadError>; + fn set_visible_layers(&self, layer_ids: Vec) -> Result<(), KiCadError>; + fn get_board_origin(&self, kind: BoardOriginKind) -> Result; + fn set_board_origin(&self, kind: BoardOriginKind, origin: Vector2Nm) -> Result<(), KiCadError>; + fn get_selection_summary(&self) -> Result; + fn get_selection_raw(&self) -> Result, KiCadError>; + fn get_selection_details(&self) -> Result, KiCadError>; + fn get_selection(&self) -> Result, KiCadError>; + fn add_to_selection_raw(&self, item_ids: Vec) -> Result, KiCadError>; + fn add_to_selection(&self, item_ids: Vec) -> Result; + fn clear_selection_raw(&self) -> Result, KiCadError>; + fn clear_selection(&self) -> Result; + fn remove_from_selection_raw(&self, item_ids: Vec) -> Result, KiCadError>; + fn remove_from_selection(&self, item_ids: Vec) -> Result; + fn get_pad_netlist(&self) -> Result, KiCadError>; + fn get_items_raw_by_type_codes(&self, type_codes: Vec) -> Result, KiCadError>; + fn get_items_details_by_type_codes(&self, type_codes: Vec) -> Result, KiCadError>; + fn get_items_by_type_codes(&self, type_codes: Vec) -> Result, KiCadError>; + fn get_all_pcb_items_raw(&self) -> Result)>, KiCadError>; + fn get_all_pcb_items_details(&self) -> Result)>, KiCadError>; + fn get_all_pcb_items(&self) -> Result)>, KiCadError>; + fn get_items_by_net_raw(&self, type_codes: Vec, net_codes: Vec) -> Result, KiCadError>; + fn get_items_by_net(&self, type_codes: Vec, net_codes: Vec) -> Result, KiCadError>; + fn get_items_by_net_class_raw(&self, type_codes: Vec, net_classes: Vec) -> Result, KiCadError>; + fn get_items_by_net_class(&self, type_codes: Vec, net_classes: Vec) -> Result, KiCadError>; + fn get_netclass_for_nets_raw(&self, nets: Vec) -> Result; + fn get_netclass_for_nets(&self, nets: Vec) -> Result, KiCadError>; + fn refill_zones(&self, zone_ids: Vec) -> Result<(), KiCadError>; + fn get_pad_shape_as_polygon_raw(&self, pad_ids: Vec, layer_id: i32) -> Result, KiCadError>; + fn get_pad_shape_as_polygon(&self, pad_ids: Vec, layer_id: i32) -> Result, KiCadError>; + fn check_padstack_presence_on_layers_raw(&self, item_ids: Vec, layer_ids: Vec) -> Result, KiCadError>; + fn check_padstack_presence_on_layers(&self, item_ids: Vec, layer_ids: Vec) -> Result, KiCadError>; + fn get_board_stackup_raw(&self) -> Result; + fn get_board_stackup(&self) -> Result; + fn update_board_stackup_raw(&self, stackup: BoardStackup) -> Result; + fn update_board_stackup(&self, stackup: BoardStackup) -> Result; + fn get_graphics_defaults_raw(&self) -> Result; + fn get_graphics_defaults(&self) -> Result; + fn get_board_editor_appearance_settings_raw(&self) -> Result; + fn get_board_editor_appearance_settings(&self) -> Result; + fn set_board_editor_appearance_settings(&self, settings: BoardEditorAppearanceSettings) -> Result; + fn interactive_move_items_raw(&self, item_ids: Vec) -> Result; + fn interactive_move_items(&self, item_ids: Vec) -> Result<(), KiCadError>; + fn get_title_block_info(&self) -> Result; + fn save_document_raw(&self) -> Result; + fn save_document(&self) -> Result<(), KiCadError>; + fn revert_document_raw(&self) -> Result; + fn revert_document(&self) -> Result<(), KiCadError>; + fn get_board_as_string(&self) -> Result; + fn get_selection_as_string(&self) -> Result; + fn get_items_by_id_raw(&self, item_ids: Vec) -> Result, KiCadError>; + fn get_items_by_id_details(&self, item_ids: Vec) -> Result, KiCadError>; + fn get_items_by_id(&self, item_ids: Vec) -> Result, KiCadError>; + fn get_item_bounding_boxes(&self, item_ids: Vec, include_child_text: bool) -> Result, KiCadError>; + fn hit_test_item(&self, item_id: String, position: Vector2Nm, tolerance_nm: i32) -> Result; + } + + #[cfg(test)] + pub(crate) const MANUAL_BLOCKING_METHOD_NAMES: &'static [&'static str] = &[ + "connect", + "run_action_raw", + "run_action", + "get_kicad_binary_path_raw", + "get_kicad_binary_path", + "get_plugin_settings_path_raw", + "get_plugin_settings_path", + "end_commit_raw", + "end_commit", + "parse_and_create_items_from_string_raw", + "parse_and_create_items_from_string", + "inject_drc_error_raw", + "inject_drc_error", + "save_copy_of_document_raw", + "save_copy_of_document", + ]; +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + use std::sync::mpsc as std_mpsc; + use std::time::{Duration, Instant}; + + #[test] + fn blocking_core_executes_job_and_returns_result() { + let core = BlockingCore::start().expect("blocking core must start"); + let value = core + .call(|_| Ok::<_, KiCadError>(1234)) + .expect("blocking job should execute"); + assert_eq!(value, 1234); + } + + #[test] + fn blocking_core_handles_concurrent_submitters() { + let core = BlockingCore::start().expect("blocking core must start"); + let mut handles = Vec::new(); + + for idx in 0..8 { + let core = Arc::clone(&core); + handles.push(thread::spawn(move || { + core.call(move |_| Ok::<_, KiCadError>(idx * 2)) + .expect("job should return"); + })); + } + + for handle in handles { + handle.join().expect("submitter thread must join"); + } + } + + #[test] + fn blocking_core_shutdown_drains_inflight_jobs() { + let core = BlockingCore::start().expect("blocking core must start"); + let (started_tx, started_rx) = std_mpsc::sync_channel::<()>(1); + + let core_for_call = Arc::clone(&core); + let worker = thread::spawn(move || { + core_for_call + .call(move |_| { + let _ = started_tx.send(()); + thread::sleep(Duration::from_millis(120)); + Ok::<_, KiCadError>(()) + }) + .expect("in-flight job should complete"); + }); + + started_rx + .recv_timeout(Duration::from_secs(1)) + .expect("job should begin"); + + let begin = Instant::now(); + core.shutdown(); + let elapsed = begin.elapsed(); + + assert!( + elapsed >= Duration::from_millis(80), + "shutdown should wait for in-flight job; elapsed: {elapsed:?}" + ); + + worker.join().expect("worker submitter should join"); + } + + #[test] + fn blocking_core_returns_closed_error_after_shutdown() { + let core = BlockingCore::start().expect("blocking core must start"); + core.shutdown(); + + let err = core + .call(|_| Ok::<_, KiCadError>(())) + .expect_err("closed core should reject calls"); + assert!(matches!(err, KiCadError::BlockingRuntimeClosed)); + } + + #[test] + fn sync_wrapper_covers_async_method_names() { + let mut async_methods = BTreeSet::new(); + for line in include_str!("client.rs").lines() { + let trimmed = line.trim_start(); + if let Some(rest) = trimmed.strip_prefix("pub async fn ") { + if let Some(name) = rest.split('(').next() { + async_methods.insert(name.trim().to_string()); + } + } + } + + let blocking_methods: BTreeSet = + KiCadClientBlocking::GENERATED_BLOCKING_METHOD_NAMES + .iter() + .chain(KiCadClientBlocking::MANUAL_BLOCKING_METHOD_NAMES.iter()) + .map(|name| (*name).to_string()) + .collect(); + + let missing: Vec = async_methods + .into_iter() + .filter(|name| !blocking_methods.contains(name)) + .collect(); + + assert!( + missing.is_empty(), + "missing blocking wrappers for async methods: {:?}", + missing + ); + } + + #[test] + fn impl_into_string_wrapper_signatures_accept_str() { + fn assert_signatures(client: &KiCadClientBlocking) { + let _ = client.run_action_raw("pcbnew.Refresh"); + let _ = client.run_action("pcbnew.Refresh"); + let _ = client.get_kicad_binary_path_raw("kicad-cli"); + let _ = client.get_kicad_binary_path("kicad-cli"); + let _ = client.get_plugin_settings_path_raw("kicad-ipc-rs"); + let _ = client.get_plugin_settings_path("kicad-ipc-rs"); + let _ = client.end_commit_raw( + CommitSession { + id: "commit-id".to_string(), + }, + CommitAction::Drop, + "test", + ); + let _ = client.end_commit( + CommitSession { + id: "commit-id".to_string(), + }, + CommitAction::Drop, + "test", + ); + let _ = client.parse_and_create_items_from_string_raw("(kicad_pcb)"); + let _ = client.parse_and_create_items_from_string("(kicad_pcb)"); + let _ = client.inject_drc_error_raw(DrcSeverity::Warning, "marker", None, Vec::new()); + let _ = client.inject_drc_error(DrcSeverity::Warning, "marker", None, Vec::new()); + let _ = client.save_copy_of_document_raw("/tmp/example.kicad_pcb", false, false); + let _ = client.save_copy_of_document("/tmp/example.kicad_pcb", false, false); + } + + let _ = assert_signatures as fn(&KiCadClientBlocking); + } + + #[test] + fn blocking_smoke_live_when_socket_env_is_set() { + if std::env::var("KICAD_API_SOCKET").is_err() { + return; + } + + let client = KiCadClientBlocking::connect().expect("blocking client should connect"); + client.ping().expect("ping should succeed"); + let _ = client.get_version().expect("version should succeed"); + let _ = client + .get_open_documents(DocumentType::Pcb) + .expect("open docs should succeed"); + let _ = client + .get_visible_layers() + .expect("board read method should succeed"); + } } diff --git a/src/error.rs b/src/error.rs index a096e1d..e35530f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,6 +52,9 @@ pub enum KiCadError { #[error("runtime task join failed: {0}")] RuntimeJoin(String), + #[error("blocking runtime is unavailable")] + BlockingRuntimeClosed, + #[error("mutex poisoned")] InternalPoisoned, diff --git a/src/lib.rs b/src/lib.rs index e57cd0f..2665109 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ pub mod blocking; pub(crate) mod proto; +#[cfg(feature = "blocking")] +pub use crate::blocking::{KiCadClientBlocking, KiCadClientBlockingBuilder}; pub use crate::client::{ClientBuilder, KiCadClient}; pub use crate::error::KiCadError; pub use crate::kicad_api_version::KICAD_API_VERSION; diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 942b6cd..d68f373 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -5,9 +5,9 @@ use std::process::ExitCode; use std::str::FromStr; use std::time::Duration; -use kicad_ipc::{ - BoardFlipMode, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, - DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, MapMergeMode, +use kicad_ipc_rs::{ + BoardFlipMode, BoardOriginKind, CommitAction, CommitSession, DocumentType, DrcSeverity, + EditorFrameType, InactiveLayerDisplayMode, KiCadClientBlocking, KiCadError, MapMergeMode, NetColorDisplayMode, PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, }; @@ -190,9 +190,8 @@ enum Command { Help, } -#[tokio::main(flavor = "current_thread")] -async fn main() -> ExitCode { - match run().await { +fn main() -> ExitCode { + match run() { Ok(()) => ExitCode::SUCCESS, Err(err) => { eprintln!("error: {err}"); @@ -216,7 +215,7 @@ async fn main() -> ExitCode { } } -async fn run() -> Result<(), KiCadError> { +fn run() -> Result<(), KiCadError> { let (config, command) = parse_args()?; if matches!(command, Command::Help) { @@ -224,7 +223,8 @@ async fn run() -> Result<(), KiCadError> { return Ok(()); } - let mut builder = ClientBuilder::new().timeout(Duration::from_millis(config.timeout_ms)); + let mut builder = + KiCadClientBlocking::builder().timeout(Duration::from_millis(config.timeout_ms)); if let Some(socket) = config.socket { builder = builder.socket_path(socket); } @@ -235,30 +235,30 @@ async fn run() -> Result<(), KiCadError> { builder = builder.client_name(client_name); } - let client = builder.connect().await?; + let client = builder.connect()?; match command { Command::Ping => { - client.ping().await?; + client.ping()?; println!("pong"); } Command::Version => { - let version = client.get_version().await?; + let version = client.get_version()?; println!( "version: {}.{}.{} ({})", version.major, version.minor, version.patch, version.full_version ); } Command::KiCadBinaryPath { binary_name } => { - let path = client.get_kicad_binary_path(binary_name).await?; + let path = client.get_kicad_binary_path(binary_name)?; println!("kicad_binary_path={path}"); } Command::PluginSettingsPath { identifier } => { - let path = client.get_plugin_settings_path(identifier).await?; + let path = client.get_plugin_settings_path(identifier)?; println!("plugin_settings_path={path}"); } Command::OpenDocs { document_type } => { - let docs = client.get_open_documents(document_type).await?; + let docs = client.get_open_documents(document_type)?; if docs.is_empty() { println!("no open `{document_type}` documents"); } else { @@ -280,11 +280,11 @@ async fn run() -> Result<(), KiCadError> { } } Command::ProjectPath => { - let path = client.get_current_project_path().await?; + let path = client.get_current_project_path()?; println!("project_path={}", path.display()); } Command::BoardOpen => { - let has_board = client.has_open_board().await?; + let has_board = client.has_open_board()?; if has_board { println!("board-open: yes"); } else { @@ -292,7 +292,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::NetClasses => { - let classes = client.get_net_classes().await?; + let classes = client.get_net_classes()?; println!("net_class_count={}", classes.len()); for class in classes { println!( @@ -308,8 +308,8 @@ async fn run() -> Result<(), KiCadError> { } } Command::SetNetClasses { merge_mode } => { - let classes = client.get_net_classes().await?; - let updated = client.set_net_classes(classes, merge_mode).await?; + let classes = client.get_net_classes()?; + let updated = client.set_net_classes(classes, merge_mode)?; println!( "net_class_count={} merge_mode={}", updated.len(), @@ -317,7 +317,7 @@ async fn run() -> Result<(), KiCadError> { ); } Command::TextVariables => { - let variables = client.get_text_variables().await?; + let variables = client.get_text_variables()?; println!("text_variable_count={}", variables.len()); for (name, value) in variables { println!("name={} value={}", name, value); @@ -327,7 +327,7 @@ async fn run() -> Result<(), KiCadError> { merge_mode, variables, } => { - let updated = client.set_text_variables(variables, merge_mode).await?; + let updated = client.set_text_variables(variables, merge_mode)?; println!( "text_variable_count={} merge_mode={}", updated.len(), @@ -338,27 +338,25 @@ async fn run() -> Result<(), KiCadError> { } } Command::ExpandTextVariables { text } => { - let expanded = client.expand_text_variables(text.clone()).await?; + let expanded = client.expand_text_variables(text.clone())?; println!("expanded_count={}", expanded.len()); for (index, value) in expanded.iter().enumerate() { println!("[{index}] input={} expanded={}", text[index], value); } } Command::TextExtents { text } => { - let extents = client.get_text_extents(TextSpec::plain(text)).await?; + let extents = client.get_text_extents(TextSpec::plain(text))?; println!( "x_nm={} y_nm={} width_nm={} height_nm={}", extents.x_nm, extents.y_nm, extents.width_nm, extents.height_nm ); } Command::TextAsShapes { text } => { - let entries = client - .get_text_as_shapes( - text.into_iter() - .map(|value| TextObjectSpec::Text(TextSpec::plain(value))) - .collect(), - ) - .await?; + let entries = client.get_text_as_shapes( + text.into_iter() + .map(|value| TextObjectSpec::Text(TextSpec::plain(value))) + .collect(), + )?; println!("text_with_shapes_count={}", entries.len()); for (index, entry) in entries.iter().enumerate() { let mut segment_count = 0; @@ -393,7 +391,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::Nets => { - let nets = client.get_nets().await?; + let nets = client.get_nets()?; if nets.is_empty() { println!("no nets returned"); } else { @@ -403,7 +401,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::EnabledLayers => { - let enabled = client.get_board_enabled_layers().await?; + let enabled = client.get_board_enabled_layers()?; println!("copper_layer_count={}", enabled.copper_layer_count); for layer in enabled.layers { println!("layer_id={} layer_name={}", layer.id, layer.name); @@ -413,27 +411,25 @@ async fn run() -> Result<(), KiCadError> { copper_layer_count, layer_ids, } => { - let enabled = client - .set_board_enabled_layers(copper_layer_count, layer_ids) - .await?; + let enabled = client.set_board_enabled_layers(copper_layer_count, layer_ids)?; println!("copper_layer_count={}", enabled.copper_layer_count); for layer in enabled.layers { println!("layer_id={} layer_name={}", layer.id, layer.name); } } Command::ActiveLayer => { - let layer = client.get_active_layer().await?; + let layer = client.get_active_layer()?; println!( "active_layer_id={} active_layer_name={}", layer.id, layer.name ); } Command::SetActiveLayer { layer_id } => { - client.set_active_layer(layer_id).await?; + client.set_active_layer(layer_id)?; println!("set_active_layer_id={}", layer_id); } Command::VisibleLayers => { - let layers = client.get_visible_layers().await?; + let layers = client.get_visible_layers()?; if layers.is_empty() { println!("no visible layers returned"); } else { @@ -443,20 +439,18 @@ async fn run() -> Result<(), KiCadError> { } } Command::SetVisibleLayers { layer_ids } => { - client.set_visible_layers(layer_ids.clone()).await?; + client.set_visible_layers(layer_ids.clone())?; println!("set_visible_layer_count={}", layer_ids.len()); } Command::BoardOrigin { kind } => { - let origin = client.get_board_origin(kind).await?; + let origin = client.get_board_origin(kind)?; println!( "origin_kind={} x_nm={} y_nm={}", kind, origin.x_nm, origin.y_nm ); } Command::SetBoardOrigin { kind, x_nm, y_nm } => { - client - .set_board_origin(kind, Vector2Nm { x_nm, y_nm }) - .await?; + client.set_board_origin(kind, Vector2Nm { x_nm, y_nm })?; println!("set_origin_kind={} x_nm={} y_nm={}", kind, x_nm, y_nm); } Command::InjectDrcError { @@ -470,20 +464,18 @@ async fn run() -> Result<(), KiCadError> { (Some(x_nm), Some(y_nm)) => Some(Vector2Nm { x_nm, y_nm }), _ => None, }; - let marker = client - .inject_drc_error(severity, message, position, item_ids) - .await?; + let marker = client.inject_drc_error(severity, message, position, item_ids)?; println!( "drc_marker_id={}", marker.unwrap_or_else(|| "-".to_string()) ); } Command::RefreshEditor { frame } => { - client.refresh_editor(frame).await?; + client.refresh_editor(frame)?; println!("refresh_editor=ok frame={}", frame); } Command::BeginCommit => { - let session = client.begin_commit().await?; + let session = client.begin_commit()?; println!("commit_id={}", session.id); } Command::EndCommit { @@ -491,13 +483,11 @@ async fn run() -> Result<(), KiCadError> { action, message, } => { - client - .end_commit(CommitSession { id }, action, message) - .await?; + client.end_commit(CommitSession { id }, action, message)?; println!("end_commit=ok action={}", action); } Command::SaveDoc => { - client.save_document().await?; + client.save_document()?; println!("save_document=ok"); } Command::SaveCopy { @@ -505,27 +495,25 @@ async fn run() -> Result<(), KiCadError> { overwrite, include_project, } => { - client - .save_copy_of_document(path, overwrite, include_project) - .await?; + client.save_copy_of_document(path, overwrite, include_project)?; println!( "save_copy_of_document=ok overwrite={} include_project={}", overwrite, include_project ); } Command::RevertDoc => { - client.revert_document().await?; + client.revert_document()?; println!("revert_document=ok"); } Command::RunAction { action } => { - let status = client.run_action(action).await?; + let status = client.run_action(action)?; println!("run_action_status={status:?}"); } Command::CreateItems { items, container_id, } => { - let created = client.create_items(items, container_id).await?; + let created = client.create_items(items, container_id)?; println!("created_item_count={}", created.len()); for (index, item) in created.iter().enumerate() { println!( @@ -536,7 +524,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::UpdateItems { items } => { - let updated = client.update_items(items).await?; + let updated = client.update_items(items)?; println!("updated_item_count={}", updated.len()); for (index, item) in updated.iter().enumerate() { println!( @@ -547,14 +535,14 @@ async fn run() -> Result<(), KiCadError> { } } Command::DeleteItems { item_ids } => { - let deleted = client.delete_items(item_ids).await?; + let deleted = client.delete_items(item_ids)?; println!("deleted_item_count={}", deleted.len()); for (index, item_id) in deleted.iter().enumerate() { println!("[{index}] id={item_id}"); } } Command::ParseCreateItemsFromString { contents } => { - let created = client.parse_and_create_items_from_string(contents).await?; + let created = client.parse_and_create_items_from_string(contents)?; println!("created_item_count={}", created.len()); for (index, item) in created.iter().enumerate() { println!( @@ -565,32 +553,32 @@ async fn run() -> Result<(), KiCadError> { } } Command::AddToSelection { item_ids } => { - let summary = client.add_to_selection(item_ids).await?; + let summary = client.add_to_selection(item_ids)?; println!("selection_total={}", summary.total_items); for entry in summary.type_url_counts { println!("type_url={} count={}", entry.type_url, entry.count); } } Command::RemoveFromSelection { item_ids } => { - let summary = client.remove_from_selection(item_ids).await?; + let summary = client.remove_from_selection(item_ids)?; println!("selection_total={}", summary.total_items); for entry in summary.type_url_counts { println!("type_url={} count={}", entry.type_url, entry.count); } } Command::ClearSelection => { - let summary = client.clear_selection().await?; + let summary = client.clear_selection()?; println!("selection_total={}", summary.total_items); } Command::SelectionSummary => { - let summary = client.get_selection_summary().await?; + let summary = client.get_selection_summary()?; println!("selection_total={}", summary.total_items); for entry in summary.type_url_counts { println!("type_url={} count={}", entry.type_url, entry.count); } } Command::SelectionDetails => { - let details = client.get_selection_details().await?; + let details = client.get_selection_details()?; println!("selection_total={}", details.len()); for (index, item) in details.iter().enumerate() { println!( @@ -600,7 +588,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::SelectionRaw => { - let items = client.get_selection_raw().await?; + let items = client.get_selection_raw()?; println!("selection_total={}", items.len()); for (index, item) in items.iter().enumerate() { println!( @@ -612,7 +600,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::NetlistPads => { - let entries = client.get_pad_netlist().await?; + let entries = client.get_pad_netlist()?; println!("pad_net_entries={}", entries.len()); for entry in entries { println!( @@ -630,7 +618,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::ItemsById { item_ids } => { - let details = client.get_items_by_id_details(item_ids).await?; + let details = client.get_items_by_id_details(item_ids)?; println!("items_total={}", details.len()); for (index, item) in details.iter().enumerate() { println!( @@ -643,9 +631,7 @@ async fn run() -> Result<(), KiCadError> { item_ids, include_child_text, } => { - let boxes = client - .get_item_bounding_boxes(item_ids, include_child_text) - .await?; + let boxes = client.get_item_bounding_boxes(item_ids, include_child_text)?; println!("bbox_total={}", boxes.len()); for entry in boxes { println!( @@ -660,13 +646,11 @@ async fn run() -> Result<(), KiCadError> { y_nm, tolerance_nm, } => { - let result = client - .hit_test_item(item_id, Vector2Nm { x_nm, y_nm }, tolerance_nm) - .await?; + let result = client.hit_test_item(item_id, Vector2Nm { x_nm, y_nm }, tolerance_nm)?; println!("hit_test={result}"); } Command::PcbTypes => { - for entry in kicad_ipc::KiCadClient::pcb_object_type_codes() { + for entry in kicad_ipc_rs::KiCadClient::pcb_object_type_codes() { println!("type_id={} type_name={}", entry.code, entry.name); } } @@ -674,9 +658,7 @@ async fn run() -> Result<(), KiCadError> { type_codes, include_debug, } => { - let items = client - .get_items_raw_by_type_codes(type_codes.clone()) - .await?; + let items = client.get_items_raw_by_type_codes(type_codes.clone())?; println!( "items_total={} requested_type_codes={:?}", items.len(), @@ -684,7 +666,7 @@ async fn run() -> Result<(), KiCadError> { ); for (index, item) in items.iter().enumerate() { if include_debug { - let debug = kicad_ipc::KiCadClient::debug_any_item(item)? + let debug = kicad_ipc_rs::KiCadClient::debug_any_item(item)? .replace('\n', "\\n") .replace('\t', " "); println!( @@ -705,11 +687,8 @@ async fn run() -> Result<(), KiCadError> { } } 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 - { + for object_type in kicad_ipc_rs::KiCadClient::pcb_object_type_codes() { + match client.get_items_raw_by_type_codes(vec![object_type.code]) { Ok(items) => { println!( "type_id={} type_name={} item_count={}", @@ -719,7 +698,7 @@ async fn run() -> Result<(), KiCadError> { ); for (index, item) in items.iter().enumerate() { if include_debug { - let debug = kicad_ipc::KiCadClient::debug_any_item(item)? + let debug = kicad_ipc_rs::KiCadClient::debug_any_item(item)? .replace('\n', "\\n") .replace('\t', " "); println!( @@ -753,9 +732,7 @@ async fn run() -> Result<(), KiCadError> { layer_id, include_debug, } => { - let rows = client - .get_pad_shape_as_polygon(pad_ids.clone(), layer_id) - .await?; + let rows = client.get_pad_shape_as_polygon(pad_ids.clone(), layer_id)?; println!( "pad_shape_total={} layer_id={} requested_pad_count={}", rows.len(), @@ -779,11 +756,9 @@ async fn run() -> Result<(), KiCadError> { ); } if include_debug { - let raw_chunks = client - .get_pad_shape_as_polygon_raw(pad_ids, layer_id) - .await?; + let raw_chunks = client.get_pad_shape_as_polygon_raw(pad_ids, layer_id)?; for (chunk_index, chunk) in raw_chunks.iter().enumerate() { - let debug = kicad_ipc::KiCadClient::debug_any_item(chunk)? + let debug = kicad_ipc_rs::KiCadClient::debug_any_item(chunk)? .replace('\n', "\\n") .replace('\t', " "); println!("raw_chunk={chunk_index} debug={debug}"); @@ -795,9 +770,8 @@ async fn run() -> Result<(), KiCadError> { layer_ids, include_debug, } => { - let rows = client - .check_padstack_presence_on_layers(item_ids.clone(), layer_ids.clone()) - .await?; + let rows = + client.check_padstack_presence_on_layers(item_ids.clone(), layer_ids.clone())?; println!( "padstack_presence_total={} requested_item_count={} requested_layer_count={}", rows.len(), @@ -811,11 +785,10 @@ async fn run() -> Result<(), KiCadError> { ); } if include_debug { - let raw_chunks = client - .check_padstack_presence_on_layers_raw(item_ids, layer_ids) - .await?; + let raw_chunks = + client.check_padstack_presence_on_layers_raw(item_ids, layer_ids)?; for (chunk_index, chunk) in raw_chunks.iter().enumerate() { - let debug = kicad_ipc::KiCadClient::debug_any_item(chunk)? + let debug = kicad_ipc_rs::KiCadClient::debug_any_item(chunk)? .replace('\n', "\\n") .replace('\t', " "); println!("raw_chunk={chunk_index} debug={debug}"); @@ -823,7 +796,7 @@ async fn run() -> Result<(), KiCadError> { } } Command::TitleBlock => { - let title_block = client.get_title_block_info().await?; + let title_block = client.get_title_block_info()?; println!("title={}", title_block.title); println!("date={}", title_block.date); println!("revision={}", title_block.revision); @@ -833,28 +806,28 @@ async fn run() -> Result<(), KiCadError> { } } Command::BoardAsString => { - let content = client.get_board_as_string().await?; + let content = client.get_board_as_string()?; println!("{content}"); } Command::SelectionAsString => { - let content = client.get_selection_as_string().await?; + let content = client.get_selection_as_string()?; println!("{content}"); } Command::Stackup => { - let stackup = client.get_board_stackup().await?; + let stackup = client.get_board_stackup()?; println!("{stackup:#?}"); } Command::UpdateStackup => { - let stackup = client.get_board_stackup().await?; - let updated = client.update_board_stackup(stackup).await?; + let stackup = client.get_board_stackup()?; + let updated = client.update_board_stackup(stackup)?; println!("{updated:#?}"); } Command::GraphicsDefaults => { - let defaults = client.get_graphics_defaults().await?; + let defaults = client.get_graphics_defaults()?; println!("{defaults:#?}"); } Command::Appearance => { - let appearance = client.get_board_editor_appearance_settings().await?; + let appearance = client.get_board_editor_appearance_settings()?; println!("{appearance:#?}"); } Command::SetAppearance { @@ -863,31 +836,31 @@ async fn run() -> Result<(), KiCadError> { board_flip, ratsnest_display, } => { - let updated = client - .set_board_editor_appearance_settings(kicad_ipc::BoardEditorAppearanceSettings { + let updated = client.set_board_editor_appearance_settings( + kicad_ipc_rs::BoardEditorAppearanceSettings { inactive_layer_display, net_color_display, board_flip, ratsnest_display, - }) - .await?; + }, + )?; println!("{updated:#?}"); } Command::RefillZones { zone_ids } => { - client.refill_zones(zone_ids).await?; + client.refill_zones(zone_ids)?; println!("refill_zones_dispatched=ok"); } Command::InteractiveMoveItems { item_ids } => { - client.interactive_move_items(item_ids.clone()).await?; + client.interactive_move_items(item_ids.clone())?; println!("interactive_move_item_count={}", item_ids.len()); } Command::NetClass => { - let nets = client.get_nets().await?; - let netclasses = client.get_netclass_for_nets(nets).await?; + let nets = client.get_nets()?; + let netclasses = client.get_netclass_for_nets(nets)?; println!("{netclasses:#?}"); } Command::BoardReadReport { output } => { - let report = build_board_read_report_markdown(&client).await?; + let report = build_board_read_report_markdown(&client)?; fs::write(&output, report).map_err(|err| KiCadError::Config { reason: format!("failed to write report to `{}`: {err}", output.display()), })?; @@ -897,9 +870,9 @@ async fn run() -> Result<(), KiCadError> { print_proto_coverage_board_read(); } Command::Smoke => { - client.ping().await?; - let version = client.get_version().await?; - let has_board = client.has_open_board().await?; + client.ping()?; + let version = client.get_version()?; + let has_board = client.has_open_board()?; println!( "smoke ok: version={}.{}.{} board_open={}", version.major, version.minor, version.patch, has_board @@ -2189,13 +2162,13 @@ TYPES: ); } -async fn build_board_read_report_markdown(client: &KiCadClient) -> Result { +fn build_board_read_report_markdown(client: &KiCadClientBlocking) -> Result { 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?; + let version = client.get_version()?; out.push_str("## Session\n\n"); out.push_str(&format!( "- KiCad version: {}.{}.{} ({})\n", @@ -2208,7 +2181,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result Result Result Result Result> = BTreeMap::new(); - let presence_rows = client - .check_padstack_presence_on_layers(pad_ids.clone(), enabled_layer_ids) - .await?; + let presence_rows = + client.check_padstack_presence_on_layers(pad_ids.clone(), enabled_layer_ids)?; out.push_str(&format!( "- presence_entry_count: {}\n", presence_rows.len() @@ -2372,9 +2340,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result Result Result = Vec::new(); - for object_type in kicad_ipc::KiCadClient::pcb_object_type_codes() { + for object_type in kicad_ipc_rs::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 - { + match client.get_items_raw_by_type_codes(vec![object_type.code]) { Ok(items) => { if items.is_empty() { missing_types.push(*object_type); @@ -2451,7 +2412,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result REPORT_MAX_ITEM_DEBUG_CHARS { debug.truncate(REPORT_MAX_ITEM_DEBUG_CHARS); debug.push_str("\n..."); @@ -2495,7 +2456,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result REPORT_MAX_BOARD_SNAPSHOT_CHARS { board_text.truncate(REPORT_MAX_BOARD_SNAPSHOT_CHARS); board_text.push_str( @@ -2665,7 +2626,7 @@ struct PolygonGeometrySummary { arc_nodes: usize, } -fn polygon_geometry_summary(polygon: &kicad_ipc::PolygonWithHolesNm) -> PolygonGeometrySummary { +fn polygon_geometry_summary(polygon: &kicad_ipc_rs::PolygonWithHolesNm) -> PolygonGeometrySummary { let mut summary = PolygonGeometrySummary { hole_count: polygon.holes.len(), ..PolygonGeometrySummary::default() @@ -2675,8 +2636,8 @@ fn polygon_geometry_summary(polygon: &kicad_ipc::PolygonWithHolesNm) -> PolygonG summary.outline_nodes = outline.nodes.len(); for node in &outline.nodes { match node { - kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, - kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1, + kicad_ipc_rs::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, + kicad_ipc_rs::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1, } } } @@ -2685,8 +2646,8 @@ fn polygon_geometry_summary(polygon: &kicad_ipc::PolygonWithHolesNm) -> PolygonG summary.hole_nodes_total += hole.nodes.len(); for node in &hole.nodes { match node { - kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, - kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1, + kicad_ipc_rs::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, + kicad_ipc_rs::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1, } } } @@ -2765,7 +2726,7 @@ fn hex_nibble(c: char) -> Result { #[cfg(test)] mod tests { use super::{parse_args_from, Command}; - use kicad_ipc::{ + use kicad_ipc_rs::{ BoardFlipMode, BoardOriginKind, CommitAction, DrcSeverity, InactiveLayerDisplayMode, NetColorDisplayMode, RatsnestDisplayMode, }; @@ -2930,7 +2891,7 @@ mod tests { match command { Command::SetNetClasses { merge_mode } => { - assert_eq!(merge_mode, kicad_ipc::MapMergeMode::Replace) + assert_eq!(merge_mode, kicad_ipc_rs::MapMergeMode::Replace) } other => panic!("unexpected command variant: {other:?}"), } @@ -2952,7 +2913,7 @@ mod tests { merge_mode, variables, } => { - assert_eq!(merge_mode, kicad_ipc::MapMergeMode::Replace); + assert_eq!(merge_mode, kicad_ipc_rs::MapMergeMode::Replace); assert_eq!(variables.get("REV").map(|value| value.as_str()), Some("A")); } other => panic!("unexpected command variant: {other:?}"),