feat(blocking): ship full sync wrapper parity

This commit is contained in:
Milind Sharma 2026-02-21 17:24:33 +08:00
parent 81d98bbfc4
commit a0271419cc
7 changed files with 915 additions and 245 deletions

View File

@ -11,6 +11,8 @@ keywords = ["kicad", "eda", "pcb", "ipc"]
categories = ["api-bindings", "asynchronous"] categories = ["api-bindings", "asynchronous"]
include = [ include = [
"/src/**", "/src/**",
"/test-scripts/kicad-ipc-cli.rs",
"/docs/TEST_CLI.md",
"/README.md", "/README.md",
"/LICENSE", "/LICENSE",
"/Cargo.toml", "/Cargo.toml",
@ -22,6 +24,11 @@ async = ["dep:nng", "dep:prost", "dep:prost-types", "dep:tokio"]
blocking = ["async"] blocking = ["async"]
tracing = ["dep:tracing"] tracing = ["dep:tracing"]
[[bin]]
name = "kicad-ipc-cli"
path = "test-scripts/kicad-ipc-cli.rs"
required-features = ["blocking"]
[dependencies] [dependencies]
nng = { version = "1.0.1", optional = true } nng = { version = "1.0.1", optional = true }
prost = { version = "0.14.3", optional = true } prost = { version = "0.14.3", optional = true }

View File

@ -8,13 +8,66 @@ Maintainer workflow: see `CONTRIBUTIONS.md`.
## Status ## Status
Alpha. `v0.1.0` release candidate. Alpha. `v0.1.1` released.
- Async API: implemented and usable. - Async API (default): implemented and usable.
- Sync/blocking wrapper API: planned, not shipped yet. - Sync/blocking wrapper API (`feature = "blocking"`): implemented with full async parity.
- Real-world user testing: still limited. - Real-world user testing: still limited.
- Issues and PRs welcome. - 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 ## Protobuf Source
This crate ships checked-in Rust protobuf output under `src/proto/generated/`. 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 ## 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 ## Runtime Compatibility Notes
@ -169,7 +223,6 @@ Legend:
## Roadmap ## Roadmap
`v0.2.0` target: `v0.2.0` target:
- Add full sync/blocking wrapper API parity over async client.
- Expand runtime + integration testing coverage. - Expand runtime + integration testing coverage.
- Set up CI to run checks/tests on commits and PRs. - Set up CI to run checks/tests on commits and PRs.
- Continue API hardening/docs/examples for stable `1.0` path. - Continue API hardening/docs/examples for stable `1.0` path.

View File

@ -6,9 +6,11 @@ CLI binary path:
Run help: Run help:
```bash ```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 ## Prereqs
1. KiCad running. 1. KiCad running.
@ -20,145 +22,145 @@ cargo run --bin kicad-ipc-cli -- help
Ping: Ping:
```bash ```bash
cargo run --bin kicad-ipc-cli -- ping cargo run --features blocking --bin kicad-ipc-cli -- ping
``` ```
Version: Version:
```bash ```bash
cargo run --bin kicad-ipc-cli -- version cargo run --features blocking --bin kicad-ipc-cli -- version
``` ```
Resolve KiCad binary path (default `kicad-cli`): Resolve KiCad binary path (default `kicad-cli`):
```bash ```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`): Resolve plugin settings path (default identifier `kicad-ipc-rust`):
```bash ```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: List open PCB docs:
```bash ```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: Check board open:
```bash ```bash
cargo run --bin kicad-ipc-cli -- board-open cargo run --features blocking --bin kicad-ipc-cli -- board-open
``` ```
List nets: List nets:
```bash ```bash
cargo run --bin kicad-ipc-cli -- nets cargo run --features blocking --bin kicad-ipc-cli -- nets
``` ```
List project net classes: List project net classes:
```bash ```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: Write current net classes back with selected merge mode:
```bash ```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: List text variables for current board document:
```bash ```bash
cargo run --bin kicad-ipc-cli -- text-variables cargo run --features blocking --bin kicad-ipc-cli -- text-variables
``` ```
Set text variables: Set text variables:
```bash ```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: Expand text variables in one or more input strings:
```bash ```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: Measure text extents:
```bash ```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: Convert text to shape primitives:
```bash ```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: List enabled board layers:
```bash ```bash
cargo run --bin kicad-ipc-cli -- enabled-layers cargo run --features blocking --bin kicad-ipc-cli -- enabled-layers
``` ```
Set enabled board layers: Set enabled board layers:
```bash ```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: Show active layer:
```bash ```bash
cargo run --bin kicad-ipc-cli -- active-layer cargo run --features blocking --bin kicad-ipc-cli -- active-layer
``` ```
Set active layer: Set active layer:
```bash ```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: Show visible layers:
```bash ```bash
cargo run --bin kicad-ipc-cli -- visible-layers cargo run --features blocking --bin kicad-ipc-cli -- visible-layers
``` ```
Set visible layers: Set visible layers:
```bash ```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): Show board origin (grid origin by default):
```bash ```bash
cargo run --bin kicad-ipc-cli -- board-origin cargo run --features blocking --bin kicad-ipc-cli -- board-origin
``` ```
Show drill origin: Show drill origin:
```bash ```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: Set board origin:
```bash ```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: Refresh PCB editor:
```bash ```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`. 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: Start a staged commit and print commit ID:
```bash ```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: End a staged commit:
```bash ```bash
cargo run --bin kicad-ipc-cli -- --client-name write-test end-commit --id <commit-id> --action drop --message "cli test cleanup" cargo run --features blocking --bin kicad-ipc-cli -- --client-name write-test end-commit --id <commit-id> --action drop --message "cli test cleanup"
``` ```
Save current board document: Save current board document:
```bash ```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: Save a copy of current board document:
```bash ```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: Revert current board document from disk:
```bash ```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: Run a raw KiCad tool action:
```bash ```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): Create raw Any item payload(s):
```bash ```bash
cargo run --bin kicad-ipc-cli -- create-items --item type.googleapis.com/kiapi.board.types.Text=<hex_payload> cargo run --features blocking --bin kicad-ipc-cli -- create-items --item type.googleapis.com/kiapi.board.types.Text=<hex_payload>
``` ```
Update raw Any item payload(s): Update raw Any item payload(s):
```bash ```bash
cargo run --bin kicad-ipc-cli -- update-items --item type.googleapis.com/kiapi.board.types.Text=<hex_payload> cargo run --features blocking --bin kicad-ipc-cli -- update-items --item type.googleapis.com/kiapi.board.types.Text=<hex_payload>
``` ```
Delete items by ID: Delete items by ID:
```bash ```bash
cargo run --bin kicad-ipc-cli -- delete-items --id <uuid> --id <uuid> cargo run --features blocking --bin kicad-ipc-cli -- delete-items --id <uuid> --id <uuid>
``` ```
Parse and create items from s-expression: Parse and create items from s-expression:
```bash ```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: Show summary of current PCB selection by item type:
```bash ```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: Show parsed details for currently selected items:
```bash ```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: Show raw protobuf payload bytes for selected items:
```bash ```bash
cargo run --bin kicad-ipc-cli -- selection-raw cargo run --features blocking --bin kicad-ipc-cli -- selection-raw
``` ```
Add items to current selection: Add items to current selection:
```bash ```bash
cargo run --bin kicad-ipc-cli -- add-to-selection --id <uuid> --id <uuid> cargo run --features blocking --bin kicad-ipc-cli -- add-to-selection --id <uuid> --id <uuid>
``` ```
Remove items from current selection: Remove items from current selection:
```bash ```bash
cargo run --bin kicad-ipc-cli -- remove-from-selection --id <uuid> --id <uuid> cargo run --features blocking --bin kicad-ipc-cli -- remove-from-selection --id <uuid> --id <uuid>
``` ```
Clear current selection: Clear current selection:
```bash ```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): Show pad-level netlist entries (footprint/pad/net):
```bash ```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: Show parsed details for specific item IDs:
```bash ```bash
cargo run --bin kicad-ipc-cli -- items-by-id --id <uuid> --id <uuid> cargo run --features blocking --bin kicad-ipc-cli -- items-by-id --id <uuid> --id <uuid>
``` ```
Show item bounding boxes: Show item bounding boxes:
```bash ```bash
cargo run --bin kicad-ipc-cli -- item-bbox --id <uuid> cargo run --features blocking --bin kicad-ipc-cli -- item-bbox --id <uuid>
``` ```
Include child text in the bounding box (for items such as footprints): Include child text in the bounding box (for items such as footprints):
```bash ```bash
cargo run --bin kicad-ipc-cli -- item-bbox --id <uuid> --include-text cargo run --features blocking --bin kicad-ipc-cli -- item-bbox --id <uuid> --include-text
``` ```
Run hit-test on a specific item: Run hit-test on a specific item:
```bash ```bash
cargo run --bin kicad-ipc-cli -- hit-test --id <uuid> --x-nm <x> --y-nm <y> --tolerance-nm 0 cargo run --features blocking --bin kicad-ipc-cli -- hit-test --id <uuid> --x-nm <x> --y-nm <y> --tolerance-nm 0
``` ```
List all PCB object type IDs from the proto enum: List all PCB object type IDs from the proto enum:
```bash ```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: Dump raw item payloads for one or more PCB object type IDs:
```bash ```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: Dump raw payloads for all PCB object classes:
```bash ```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: Check whether pads/vias have flashed padstack shapes on specific layers:
```bash ```bash
cargo run --bin kicad-ipc-cli -- padstack-presence --item-id <uuid> --layer-id 3 --layer-id 34 --debug cargo run --features blocking --bin kicad-ipc-cli -- padstack-presence --item-id <uuid> --layer-id 3 --layer-id 34 --debug
``` ```
Get polygonized pad shape(s) on a specific layer: Get polygonized pad shape(s) on a specific layer:
```bash ```bash
cargo run --bin kicad-ipc-cli -- pad-shape-polygon --pad-id <uuid> --layer-id 3 --debug cargo run --features blocking --bin kicad-ipc-cli -- pad-shape-polygon --pad-id <uuid> --layer-id 3 --debug
``` ```
Dump board text (KiCad s-expression): Dump board text (KiCad s-expression):
```bash ```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): Dump selection text (KiCad s-expression):
```bash ```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: Dump title block fields:
```bash ```bash
cargo run --bin kicad-ipc-cli -- title-block cargo run --features blocking --bin kicad-ipc-cli -- title-block
``` ```
Show typed stackup/graphics/appearance: Show typed stackup/graphics/appearance:
```bash ```bash
cargo run --bin kicad-ipc-cli -- stackup cargo run --features blocking --bin kicad-ipc-cli -- stackup
cargo run --bin kicad-ipc-cli -- update-stackup cargo run --features blocking --bin kicad-ipc-cli -- update-stackup
cargo run --bin kicad-ipc-cli -- graphics-defaults cargo run --features blocking --bin kicad-ipc-cli -- graphics-defaults
cargo run --bin kicad-ipc-cli -- appearance cargo run --features blocking --bin kicad-ipc-cli -- appearance
``` ```
Set editor appearance: Set editor appearance:
```bash ```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: Inject DRC marker:
```bash ```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: Refill all zones:
```bash ```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: Start interactive move tool for one or more item IDs:
```bash ```bash
cargo run --bin kicad-ipc-cli -- interactive-move --id <uuid> --id <uuid> cargo run --features blocking --bin kicad-ipc-cli -- interactive-move --id <uuid> --id <uuid>
``` ```
Show typed netclass map: Show typed netclass map:
```bash ```bash
cargo run --bin kicad-ipc-cli -- netclass cargo run --features blocking --bin kicad-ipc-cli -- netclass
``` ```
Print proto command coverage status (board read): Print proto command coverage status (board read):
```bash ```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: Generate full board-read reconstruction markdown report:
```bash ```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: Notes:
@ -395,13 +397,13 @@ Notes:
Get current project path (derived from open PCB docs): Get current project path (derived from open PCB docs):
```bash ```bash
cargo run --bin kicad-ipc-cli -- project-path cargo run --features blocking --bin kicad-ipc-cli -- project-path
``` ```
Smoke check: Smoke check:
```bash ```bash
cargo run --bin kicad-ipc-cli -- smoke cargo run --features blocking --bin kicad-ipc-cli -- smoke
``` ```
## Common Flags ## Common Flags
@ -409,25 +411,25 @@ cargo run --bin kicad-ipc-cli -- smoke
Custom socket: Custom socket:
```bash ```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: Custom token:
```bash ```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): Stable client name (needed when pairing `begin-commit` and `end-commit` across separate CLI runs):
```bash ```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: Custom timeout:
```bash ```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 ## Failure Hints

View File

@ -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::error::KiCadError;
use crate::model::board::*;
use crate::model::common::*;
const BLOCKING_QUEUE_CAPACITY: usize = 64;
type Job = Box<dyn FnOnce(&tokio::runtime::Runtime) + Send + 'static>;
#[derive(Debug)]
struct BlockingCore {
job_tx: Mutex<Option<SyncSender<Job>>>,
worker_thread_id: ThreadId,
worker_join: Mutex<Option<JoinHandle<()>>>,
}
impl BlockingCore {
fn start() -> Result<Arc<Self>, KiCadError> {
let (job_tx, job_rx) = mpsc::sync_channel::<Job>(BLOCKING_QUEUE_CAPACITY);
let (init_tx, init_rx) = mpsc::sync_channel::<Result<ThreadId, KiCadError>>(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<T, F>(&self, f: F) -> Result<T, KiCadError>
where
T: Send + 'static,
F: FnOnce(&tokio::runtime::Runtime) -> Result<T, KiCadError> + 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::<Result<T, KiCadError>>(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)] #[derive(Clone, Debug)]
pub struct KiCadClientBlocking { pub struct KiCadClientBlocking {
inner: KiCadClient, inner: KiCadClient,
core: Arc<BlockingCore>,
}
#[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<String>) -> Self {
self.inner = self.inner.socket_path(socket_path);
self
}
pub fn token(mut self, token: impl Into<String>) -> Self {
self.inner = self.inner.token(token);
self
}
pub fn client_name(mut self, client_name: impl Into<String>) -> Self {
self.inner = self.inner.client_name(client_name);
self
}
pub fn connect(self) -> Result<KiCadClientBlocking, KiCadError> {
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 { impl KiCadClientBlocking {
pub fn builder() -> KiCadClientBlockingBuilder {
KiCadClientBlockingBuilder::new()
}
pub fn connect() -> Result<Self, KiCadError> { pub fn connect() -> Result<Self, KiCadError> {
let runtime = tokio::runtime::Builder::new_current_thread() KiCadClientBlockingBuilder::new().connect()
.enable_time() }
.build()
.map_err(|err| KiCadError::RuntimeJoin(err.to_string()))?; pub fn timeout(&self) -> Duration {
let inner = runtime.block_on(KiCadClient::connect())?; self.inner.timeout()
Ok(Self { inner }) }
pub fn socket_uri(&self) -> &str {
self.inner.socket_uri()
} }
pub fn inner(&self) -> &KiCadClient { pub fn inner(&self) -> &KiCadClient {
&self.inner &self.inner
} }
pub fn run_action_raw(&self, action: impl Into<String>) -> Result<Any, KiCadError> {
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<String>) -> Result<RunActionStatus, KiCadError> {
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<String>,
) -> Result<Any, KiCadError> {
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<String>,
) -> Result<String, KiCadError> {
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<String>,
) -> Result<Any, KiCadError> {
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<String>,
) -> Result<String, KiCadError> {
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<String>,
) -> Result<Any, KiCadError> {
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<String>,
) -> 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<String>,
) -> Result<Any, 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_raw(contents)
.await
})
})
}
pub fn parse_and_create_items_from_string(
&self,
contents: impl Into<String>,
) -> Result<Vec<Any>, 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<String>,
position: Option<Vector2Nm>,
item_ids: Vec<String>,
) -> Result<Any, KiCadError> {
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<String>,
position: Option<Vector2Nm>,
item_ids: Vec<String>,
) -> Result<Option<String>, 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<String>,
overwrite: bool,
include_project: bool,
) -> Result<Any, 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_raw(path, overwrite, include_project)
.await
})
})
}
pub fn save_copy_of_document(
&self,
path: impl Into<String>,
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<VersionInfo, KiCadError>;
fn get_open_documents(&self, document_type: DocumentType) -> Result<Vec<DocumentSpecifier>, KiCadError>;
fn get_net_classes_raw(&self) -> Result<Any, KiCadError>;
fn get_net_classes(&self) -> Result<Vec<NetClassInfo>, KiCadError>;
fn set_net_classes_raw(&self, net_classes: Vec<NetClassInfo>, merge_mode: MapMergeMode) -> Result<Any, KiCadError>;
fn set_net_classes(&self, net_classes: Vec<NetClassInfo>, merge_mode: MapMergeMode) -> Result<Vec<NetClassInfo>, KiCadError>;
fn get_text_variables_raw(&self) -> Result<Any, KiCadError>;
fn get_text_variables(&self) -> Result<BTreeMap<String, String>, KiCadError>;
fn set_text_variables_raw(&self, variables: BTreeMap<String, String>, merge_mode: MapMergeMode) -> Result<Any, KiCadError>;
fn set_text_variables(&self, variables: BTreeMap<String, String>, merge_mode: MapMergeMode) -> Result<BTreeMap<String, String>, KiCadError>;
fn expand_text_variables_raw(&self, text: Vec<String>) -> Result<Any, KiCadError>;
fn expand_text_variables(&self, text: Vec<String>) -> Result<Vec<String>, KiCadError>;
fn get_text_extents_raw(&self, text: TextSpec) -> Result<Any, KiCadError>;
fn get_text_extents(&self, text: TextSpec) -> Result<TextExtents, KiCadError>;
fn get_text_as_shapes_raw(&self, text: Vec<TextObjectSpec>) -> Result<Any, KiCadError>;
fn get_text_as_shapes(&self, text: Vec<TextObjectSpec>) -> Result<Vec<TextAsShapesEntry>, KiCadError>;
fn get_current_project_path(&self) -> Result<PathBuf, KiCadError>;
fn has_open_board(&self) -> Result<bool, KiCadError>;
fn begin_commit_raw(&self) -> Result<Any, KiCadError>;
fn begin_commit(&self) -> Result<CommitSession, KiCadError>;
fn create_items_raw(&self, items: Vec<Any>, container_id: Option<String>) -> Result<Any, KiCadError>;
fn create_items(&self, items: Vec<Any>, container_id: Option<String>) -> Result<Vec<Any>, KiCadError>;
fn update_items_raw(&self, items: Vec<Any>) -> Result<Any, KiCadError>;
fn update_items(&self, items: Vec<Any>) -> Result<Vec<Any>, KiCadError>;
fn delete_items_raw(&self, item_ids: Vec<String>) -> Result<Any, KiCadError>;
fn delete_items(&self, item_ids: Vec<String>) -> Result<Vec<String>, KiCadError>;
fn get_nets(&self) -> Result<Vec<BoardNet>, KiCadError>;
fn get_board_enabled_layers(&self) -> Result<BoardEnabledLayers, KiCadError>;
fn set_board_enabled_layers(&self, copper_layer_count: u32, layer_ids: Vec<i32>) -> Result<BoardEnabledLayers, KiCadError>;
fn get_active_layer(&self) -> Result<BoardLayerInfo, KiCadError>;
fn set_active_layer(&self, layer_id: i32) -> Result<(), KiCadError>;
fn get_visible_layers(&self) -> Result<Vec<BoardLayerInfo>, KiCadError>;
fn set_visible_layers(&self, layer_ids: Vec<i32>) -> Result<(), KiCadError>;
fn get_board_origin(&self, kind: BoardOriginKind) -> Result<Vector2Nm, KiCadError>;
fn set_board_origin(&self, kind: BoardOriginKind, origin: Vector2Nm) -> Result<(), KiCadError>;
fn get_selection_summary(&self) -> Result<SelectionSummary, KiCadError>;
fn get_selection_raw(&self) -> Result<Vec<Any>, KiCadError>;
fn get_selection_details(&self) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_selection(&self) -> Result<Vec<PcbItem>, KiCadError>;
fn add_to_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn add_to_selection(&self, item_ids: Vec<String>) -> Result<SelectionSummary, KiCadError>;
fn clear_selection_raw(&self) -> Result<Vec<Any>, KiCadError>;
fn clear_selection(&self) -> Result<SelectionSummary, KiCadError>;
fn remove_from_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn remove_from_selection(&self, item_ids: Vec<String>) -> Result<SelectionSummary, KiCadError>;
fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError>;
fn get_items_raw_by_type_codes(&self, type_codes: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn get_items_details_by_type_codes(&self, type_codes: Vec<i32>) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_items_by_type_codes(&self, type_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_all_pcb_items_raw(&self) -> Result<Vec<(PcbObjectTypeCode, Vec<Any>)>, KiCadError>;
fn get_all_pcb_items_details(&self) -> Result<Vec<(PcbObjectTypeCode, Vec<SelectionItemDetail>)>, KiCadError>;
fn get_all_pcb_items(&self) -> Result<Vec<(PcbObjectTypeCode, Vec<PcbItem>)>, KiCadError>;
fn get_items_by_net_raw(&self, type_codes: Vec<i32>, net_codes: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn get_items_by_net(&self, type_codes: Vec<i32>, net_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_items_by_net_class_raw(&self, type_codes: Vec<i32>, net_classes: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn get_items_by_net_class(&self, type_codes: Vec<i32>, net_classes: Vec<String>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_netclass_for_nets_raw(&self, nets: Vec<BoardNet>) -> Result<Any, KiCadError>;
fn get_netclass_for_nets(&self, nets: Vec<BoardNet>) -> Result<Vec<NetClassForNetEntry>, KiCadError>;
fn refill_zones(&self, zone_ids: Vec<String>) -> Result<(), KiCadError>;
fn get_pad_shape_as_polygon_raw(&self, pad_ids: Vec<String>, layer_id: i32) -> Result<Vec<Any>, KiCadError>;
fn get_pad_shape_as_polygon(&self, pad_ids: Vec<String>, layer_id: i32) -> Result<Vec<PadShapeAsPolygonEntry>, KiCadError>;
fn check_padstack_presence_on_layers_raw(&self, item_ids: Vec<String>, layer_ids: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn check_padstack_presence_on_layers(&self, item_ids: Vec<String>, layer_ids: Vec<i32>) -> Result<Vec<PadstackPresenceEntry>, KiCadError>;
fn get_board_stackup_raw(&self) -> Result<Any, KiCadError>;
fn get_board_stackup(&self) -> Result<BoardStackup, KiCadError>;
fn update_board_stackup_raw(&self, stackup: BoardStackup) -> Result<Any, KiCadError>;
fn update_board_stackup(&self, stackup: BoardStackup) -> Result<BoardStackup, KiCadError>;
fn get_graphics_defaults_raw(&self) -> Result<Any, KiCadError>;
fn get_graphics_defaults(&self) -> Result<GraphicsDefaults, KiCadError>;
fn get_board_editor_appearance_settings_raw(&self) -> Result<Any, KiCadError>;
fn get_board_editor_appearance_settings(&self) -> Result<BoardEditorAppearanceSettings, KiCadError>;
fn set_board_editor_appearance_settings(&self, settings: BoardEditorAppearanceSettings) -> Result<BoardEditorAppearanceSettings, KiCadError>;
fn interactive_move_items_raw(&self, item_ids: Vec<String>) -> Result<Any, KiCadError>;
fn interactive_move_items(&self, item_ids: Vec<String>) -> Result<(), KiCadError>;
fn get_title_block_info(&self) -> Result<TitleBlockInfo, KiCadError>;
fn save_document_raw(&self) -> Result<Any, KiCadError>;
fn save_document(&self) -> Result<(), KiCadError>;
fn revert_document_raw(&self) -> Result<Any, KiCadError>;
fn revert_document(&self) -> Result<(), KiCadError>;
fn get_board_as_string(&self) -> Result<String, KiCadError>;
fn get_selection_as_string(&self) -> Result<String, KiCadError>;
fn get_items_by_id_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn get_items_by_id_details(&self, item_ids: Vec<String>) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_items_by_id(&self, item_ids: Vec<String>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_item_bounding_boxes(&self, item_ids: Vec<String>, include_child_text: bool) -> Result<Vec<ItemBoundingBox>, KiCadError>;
fn hit_test_item(&self, item_id: String, position: Vector2Nm, tolerance_nm: i32) -> Result<ItemHitTestResult, KiCadError>;
}
#[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<String> =
KiCadClientBlocking::GENERATED_BLOCKING_METHOD_NAMES
.iter()
.chain(KiCadClientBlocking::MANUAL_BLOCKING_METHOD_NAMES.iter())
.map(|name| (*name).to_string())
.collect();
let missing: Vec<String> = 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");
}
} }

View File

@ -52,6 +52,9 @@ pub enum KiCadError {
#[error("runtime task join failed: {0}")] #[error("runtime task join failed: {0}")]
RuntimeJoin(String), RuntimeJoin(String),
#[error("blocking runtime is unavailable")]
BlockingRuntimeClosed,
#[error("mutex poisoned")] #[error("mutex poisoned")]
InternalPoisoned, InternalPoisoned,

View File

@ -19,6 +19,8 @@ pub mod blocking;
pub(crate) mod proto; pub(crate) mod proto;
#[cfg(feature = "blocking")]
pub use crate::blocking::{KiCadClientBlocking, KiCadClientBlockingBuilder};
pub use crate::client::{ClientBuilder, KiCadClient}; pub use crate::client::{ClientBuilder, KiCadClient};
pub use crate::error::KiCadError; pub use crate::error::KiCadError;
pub use crate::kicad_api_version::KICAD_API_VERSION; pub use crate::kicad_api_version::KICAD_API_VERSION;

View File

@ -5,9 +5,9 @@ use std::process::ExitCode;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use kicad_ipc::{ use kicad_ipc_rs::{
BoardFlipMode, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, BoardFlipMode, BoardOriginKind, CommitAction, CommitSession, DocumentType, DrcSeverity,
DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, MapMergeMode, EditorFrameType, InactiveLayerDisplayMode, KiCadClientBlocking, KiCadError, MapMergeMode,
NetColorDisplayMode, PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, NetColorDisplayMode, PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode,
TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm,
}; };
@ -190,9 +190,8 @@ enum Command {
Help, Help,
} }
#[tokio::main(flavor = "current_thread")] fn main() -> ExitCode {
async fn main() -> ExitCode { match run() {
match run().await {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(err) => { Err(err) => {
eprintln!("error: {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()?; let (config, command) = parse_args()?;
if matches!(command, Command::Help) { if matches!(command, Command::Help) {
@ -224,7 +223,8 @@ async fn run() -> Result<(), KiCadError> {
return Ok(()); 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 { if let Some(socket) = config.socket {
builder = builder.socket_path(socket); builder = builder.socket_path(socket);
} }
@ -235,30 +235,30 @@ async fn run() -> Result<(), KiCadError> {
builder = builder.client_name(client_name); builder = builder.client_name(client_name);
} }
let client = builder.connect().await?; let client = builder.connect()?;
match command { match command {
Command::Ping => { Command::Ping => {
client.ping().await?; client.ping()?;
println!("pong"); println!("pong");
} }
Command::Version => { Command::Version => {
let version = client.get_version().await?; let version = client.get_version()?;
println!( println!(
"version: {}.{}.{} ({})", "version: {}.{}.{} ({})",
version.major, version.minor, version.patch, version.full_version version.major, version.minor, version.patch, version.full_version
); );
} }
Command::KiCadBinaryPath { binary_name } => { 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}"); println!("kicad_binary_path={path}");
} }
Command::PluginSettingsPath { identifier } => { 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}"); println!("plugin_settings_path={path}");
} }
Command::OpenDocs { document_type } => { 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() { if docs.is_empty() {
println!("no open `{document_type}` documents"); println!("no open `{document_type}` documents");
} else { } else {
@ -280,11 +280,11 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::ProjectPath => { Command::ProjectPath => {
let path = client.get_current_project_path().await?; let path = client.get_current_project_path()?;
println!("project_path={}", path.display()); println!("project_path={}", path.display());
} }
Command::BoardOpen => { Command::BoardOpen => {
let has_board = client.has_open_board().await?; let has_board = client.has_open_board()?;
if has_board { if has_board {
println!("board-open: yes"); println!("board-open: yes");
} else { } else {
@ -292,7 +292,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::NetClasses => { Command::NetClasses => {
let classes = client.get_net_classes().await?; let classes = client.get_net_classes()?;
println!("net_class_count={}", classes.len()); println!("net_class_count={}", classes.len());
for class in classes { for class in classes {
println!( println!(
@ -308,8 +308,8 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::SetNetClasses { merge_mode } => { Command::SetNetClasses { merge_mode } => {
let classes = client.get_net_classes().await?; let classes = client.get_net_classes()?;
let updated = client.set_net_classes(classes, merge_mode).await?; let updated = client.set_net_classes(classes, merge_mode)?;
println!( println!(
"net_class_count={} merge_mode={}", "net_class_count={} merge_mode={}",
updated.len(), updated.len(),
@ -317,7 +317,7 @@ async fn run() -> Result<(), KiCadError> {
); );
} }
Command::TextVariables => { Command::TextVariables => {
let variables = client.get_text_variables().await?; let variables = client.get_text_variables()?;
println!("text_variable_count={}", variables.len()); println!("text_variable_count={}", variables.len());
for (name, value) in variables { for (name, value) in variables {
println!("name={} value={}", name, value); println!("name={} value={}", name, value);
@ -327,7 +327,7 @@ async fn run() -> Result<(), KiCadError> {
merge_mode, merge_mode,
variables, variables,
} => { } => {
let updated = client.set_text_variables(variables, merge_mode).await?; let updated = client.set_text_variables(variables, merge_mode)?;
println!( println!(
"text_variable_count={} merge_mode={}", "text_variable_count={} merge_mode={}",
updated.len(), updated.len(),
@ -338,27 +338,25 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::ExpandTextVariables { text } => { 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()); println!("expanded_count={}", expanded.len());
for (index, value) in expanded.iter().enumerate() { for (index, value) in expanded.iter().enumerate() {
println!("[{index}] input={} expanded={}", text[index], value); println!("[{index}] input={} expanded={}", text[index], value);
} }
} }
Command::TextExtents { text } => { Command::TextExtents { text } => {
let extents = client.get_text_extents(TextSpec::plain(text)).await?; let extents = client.get_text_extents(TextSpec::plain(text))?;
println!( println!(
"x_nm={} y_nm={} width_nm={} height_nm={}", "x_nm={} y_nm={} width_nm={} height_nm={}",
extents.x_nm, extents.y_nm, extents.width_nm, extents.height_nm extents.x_nm, extents.y_nm, extents.width_nm, extents.height_nm
); );
} }
Command::TextAsShapes { text } => { Command::TextAsShapes { text } => {
let entries = client let entries = client.get_text_as_shapes(
.get_text_as_shapes(
text.into_iter() text.into_iter()
.map(|value| TextObjectSpec::Text(TextSpec::plain(value))) .map(|value| TextObjectSpec::Text(TextSpec::plain(value)))
.collect(), .collect(),
) )?;
.await?;
println!("text_with_shapes_count={}", entries.len()); println!("text_with_shapes_count={}", entries.len());
for (index, entry) in entries.iter().enumerate() { for (index, entry) in entries.iter().enumerate() {
let mut segment_count = 0; let mut segment_count = 0;
@ -393,7 +391,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::Nets => { Command::Nets => {
let nets = client.get_nets().await?; let nets = client.get_nets()?;
if nets.is_empty() { if nets.is_empty() {
println!("no nets returned"); println!("no nets returned");
} else { } else {
@ -403,7 +401,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::EnabledLayers => { 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); println!("copper_layer_count={}", enabled.copper_layer_count);
for layer in enabled.layers { for layer in enabled.layers {
println!("layer_id={} layer_name={}", layer.id, layer.name); println!("layer_id={} layer_name={}", layer.id, layer.name);
@ -413,27 +411,25 @@ async fn run() -> Result<(), KiCadError> {
copper_layer_count, copper_layer_count,
layer_ids, layer_ids,
} => { } => {
let enabled = client let enabled = client.set_board_enabled_layers(copper_layer_count, layer_ids)?;
.set_board_enabled_layers(copper_layer_count, layer_ids)
.await?;
println!("copper_layer_count={}", enabled.copper_layer_count); println!("copper_layer_count={}", enabled.copper_layer_count);
for layer in enabled.layers { for layer in enabled.layers {
println!("layer_id={} layer_name={}", layer.id, layer.name); println!("layer_id={} layer_name={}", layer.id, layer.name);
} }
} }
Command::ActiveLayer => { Command::ActiveLayer => {
let layer = client.get_active_layer().await?; let layer = client.get_active_layer()?;
println!( println!(
"active_layer_id={} active_layer_name={}", "active_layer_id={} active_layer_name={}",
layer.id, layer.name layer.id, layer.name
); );
} }
Command::SetActiveLayer { layer_id } => { Command::SetActiveLayer { layer_id } => {
client.set_active_layer(layer_id).await?; client.set_active_layer(layer_id)?;
println!("set_active_layer_id={}", layer_id); println!("set_active_layer_id={}", layer_id);
} }
Command::VisibleLayers => { Command::VisibleLayers => {
let layers = client.get_visible_layers().await?; let layers = client.get_visible_layers()?;
if layers.is_empty() { if layers.is_empty() {
println!("no visible layers returned"); println!("no visible layers returned");
} else { } else {
@ -443,20 +439,18 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::SetVisibleLayers { layer_ids } => { 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()); println!("set_visible_layer_count={}", layer_ids.len());
} }
Command::BoardOrigin { kind } => { Command::BoardOrigin { kind } => {
let origin = client.get_board_origin(kind).await?; let origin = client.get_board_origin(kind)?;
println!( println!(
"origin_kind={} x_nm={} y_nm={}", "origin_kind={} x_nm={} y_nm={}",
kind, origin.x_nm, origin.y_nm kind, origin.x_nm, origin.y_nm
); );
} }
Command::SetBoardOrigin { kind, x_nm, y_nm } => { Command::SetBoardOrigin { kind, x_nm, y_nm } => {
client client.set_board_origin(kind, Vector2Nm { x_nm, y_nm })?;
.set_board_origin(kind, Vector2Nm { x_nm, y_nm })
.await?;
println!("set_origin_kind={} x_nm={} y_nm={}", kind, x_nm, y_nm); println!("set_origin_kind={} x_nm={} y_nm={}", kind, x_nm, y_nm);
} }
Command::InjectDrcError { Command::InjectDrcError {
@ -470,20 +464,18 @@ async fn run() -> Result<(), KiCadError> {
(Some(x_nm), Some(y_nm)) => Some(Vector2Nm { x_nm, y_nm }), (Some(x_nm), Some(y_nm)) => Some(Vector2Nm { x_nm, y_nm }),
_ => None, _ => None,
}; };
let marker = client let marker = client.inject_drc_error(severity, message, position, item_ids)?;
.inject_drc_error(severity, message, position, item_ids)
.await?;
println!( println!(
"drc_marker_id={}", "drc_marker_id={}",
marker.unwrap_or_else(|| "-".to_string()) marker.unwrap_or_else(|| "-".to_string())
); );
} }
Command::RefreshEditor { frame } => { Command::RefreshEditor { frame } => {
client.refresh_editor(frame).await?; client.refresh_editor(frame)?;
println!("refresh_editor=ok frame={}", frame); println!("refresh_editor=ok frame={}", frame);
} }
Command::BeginCommit => { Command::BeginCommit => {
let session = client.begin_commit().await?; let session = client.begin_commit()?;
println!("commit_id={}", session.id); println!("commit_id={}", session.id);
} }
Command::EndCommit { Command::EndCommit {
@ -491,13 +483,11 @@ async fn run() -> Result<(), KiCadError> {
action, action,
message, message,
} => { } => {
client client.end_commit(CommitSession { id }, action, message)?;
.end_commit(CommitSession { id }, action, message)
.await?;
println!("end_commit=ok action={}", action); println!("end_commit=ok action={}", action);
} }
Command::SaveDoc => { Command::SaveDoc => {
client.save_document().await?; client.save_document()?;
println!("save_document=ok"); println!("save_document=ok");
} }
Command::SaveCopy { Command::SaveCopy {
@ -505,27 +495,25 @@ async fn run() -> Result<(), KiCadError> {
overwrite, overwrite,
include_project, include_project,
} => { } => {
client client.save_copy_of_document(path, overwrite, include_project)?;
.save_copy_of_document(path, overwrite, include_project)
.await?;
println!( println!(
"save_copy_of_document=ok overwrite={} include_project={}", "save_copy_of_document=ok overwrite={} include_project={}",
overwrite, include_project overwrite, include_project
); );
} }
Command::RevertDoc => { Command::RevertDoc => {
client.revert_document().await?; client.revert_document()?;
println!("revert_document=ok"); println!("revert_document=ok");
} }
Command::RunAction { action } => { Command::RunAction { action } => {
let status = client.run_action(action).await?; let status = client.run_action(action)?;
println!("run_action_status={status:?}"); println!("run_action_status={status:?}");
} }
Command::CreateItems { Command::CreateItems {
items, items,
container_id, 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()); println!("created_item_count={}", created.len());
for (index, item) in created.iter().enumerate() { for (index, item) in created.iter().enumerate() {
println!( println!(
@ -536,7 +524,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::UpdateItems { items } => { Command::UpdateItems { items } => {
let updated = client.update_items(items).await?; let updated = client.update_items(items)?;
println!("updated_item_count={}", updated.len()); println!("updated_item_count={}", updated.len());
for (index, item) in updated.iter().enumerate() { for (index, item) in updated.iter().enumerate() {
println!( println!(
@ -547,14 +535,14 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::DeleteItems { item_ids } => { 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()); println!("deleted_item_count={}", deleted.len());
for (index, item_id) in deleted.iter().enumerate() { for (index, item_id) in deleted.iter().enumerate() {
println!("[{index}] id={item_id}"); println!("[{index}] id={item_id}");
} }
} }
Command::ParseCreateItemsFromString { contents } => { 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()); println!("created_item_count={}", created.len());
for (index, item) in created.iter().enumerate() { for (index, item) in created.iter().enumerate() {
println!( println!(
@ -565,32 +553,32 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::AddToSelection { item_ids } => { 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); println!("selection_total={}", summary.total_items);
for entry in summary.type_url_counts { for entry in summary.type_url_counts {
println!("type_url={} count={}", entry.type_url, entry.count); println!("type_url={} count={}", entry.type_url, entry.count);
} }
} }
Command::RemoveFromSelection { item_ids } => { 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); println!("selection_total={}", summary.total_items);
for entry in summary.type_url_counts { for entry in summary.type_url_counts {
println!("type_url={} count={}", entry.type_url, entry.count); println!("type_url={} count={}", entry.type_url, entry.count);
} }
} }
Command::ClearSelection => { Command::ClearSelection => {
let summary = client.clear_selection().await?; let summary = client.clear_selection()?;
println!("selection_total={}", summary.total_items); println!("selection_total={}", summary.total_items);
} }
Command::SelectionSummary => { Command::SelectionSummary => {
let summary = client.get_selection_summary().await?; let summary = client.get_selection_summary()?;
println!("selection_total={}", summary.total_items); println!("selection_total={}", summary.total_items);
for entry in summary.type_url_counts { for entry in summary.type_url_counts {
println!("type_url={} count={}", entry.type_url, entry.count); println!("type_url={} count={}", entry.type_url, entry.count);
} }
} }
Command::SelectionDetails => { Command::SelectionDetails => {
let details = client.get_selection_details().await?; let details = client.get_selection_details()?;
println!("selection_total={}", details.len()); println!("selection_total={}", details.len());
for (index, item) in details.iter().enumerate() { for (index, item) in details.iter().enumerate() {
println!( println!(
@ -600,7 +588,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::SelectionRaw => { Command::SelectionRaw => {
let items = client.get_selection_raw().await?; let items = client.get_selection_raw()?;
println!("selection_total={}", items.len()); println!("selection_total={}", items.len());
for (index, item) in items.iter().enumerate() { for (index, item) in items.iter().enumerate() {
println!( println!(
@ -612,7 +600,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::NetlistPads => { Command::NetlistPads => {
let entries = client.get_pad_netlist().await?; let entries = client.get_pad_netlist()?;
println!("pad_net_entries={}", entries.len()); println!("pad_net_entries={}", entries.len());
for entry in entries { for entry in entries {
println!( println!(
@ -630,7 +618,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::ItemsById { item_ids } => { 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()); println!("items_total={}", details.len());
for (index, item) in details.iter().enumerate() { for (index, item) in details.iter().enumerate() {
println!( println!(
@ -643,9 +631,7 @@ async fn run() -> Result<(), KiCadError> {
item_ids, item_ids,
include_child_text, include_child_text,
} => { } => {
let boxes = client let boxes = client.get_item_bounding_boxes(item_ids, include_child_text)?;
.get_item_bounding_boxes(item_ids, include_child_text)
.await?;
println!("bbox_total={}", boxes.len()); println!("bbox_total={}", boxes.len());
for entry in boxes { for entry in boxes {
println!( println!(
@ -660,13 +646,11 @@ async fn run() -> Result<(), KiCadError> {
y_nm, y_nm,
tolerance_nm, tolerance_nm,
} => { } => {
let result = client let result = client.hit_test_item(item_id, Vector2Nm { x_nm, y_nm }, tolerance_nm)?;
.hit_test_item(item_id, Vector2Nm { x_nm, y_nm }, tolerance_nm)
.await?;
println!("hit_test={result}"); println!("hit_test={result}");
} }
Command::PcbTypes => { 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); println!("type_id={} type_name={}", entry.code, entry.name);
} }
} }
@ -674,9 +658,7 @@ async fn run() -> Result<(), KiCadError> {
type_codes, type_codes,
include_debug, include_debug,
} => { } => {
let items = client let items = client.get_items_raw_by_type_codes(type_codes.clone())?;
.get_items_raw_by_type_codes(type_codes.clone())
.await?;
println!( println!(
"items_total={} requested_type_codes={:?}", "items_total={} requested_type_codes={:?}",
items.len(), items.len(),
@ -684,7 +666,7 @@ async fn run() -> Result<(), KiCadError> {
); );
for (index, item) in items.iter().enumerate() { for (index, item) in items.iter().enumerate() {
if include_debug { 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('\n', "\\n")
.replace('\t', " "); .replace('\t', " ");
println!( println!(
@ -705,11 +687,8 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::ItemsRawAllPcb { include_debug } => { Command::ItemsRawAllPcb { include_debug } => {
for object_type in kicad_ipc::KiCadClient::pcb_object_type_codes() { for object_type in kicad_ipc_rs::KiCadClient::pcb_object_type_codes() {
match client match client.get_items_raw_by_type_codes(vec![object_type.code]) {
.get_items_raw_by_type_codes(vec![object_type.code])
.await
{
Ok(items) => { Ok(items) => {
println!( println!(
"type_id={} type_name={} item_count={}", "type_id={} type_name={} item_count={}",
@ -719,7 +698,7 @@ async fn run() -> Result<(), KiCadError> {
); );
for (index, item) in items.iter().enumerate() { for (index, item) in items.iter().enumerate() {
if include_debug { 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('\n', "\\n")
.replace('\t', " "); .replace('\t', " ");
println!( println!(
@ -753,9 +732,7 @@ async fn run() -> Result<(), KiCadError> {
layer_id, layer_id,
include_debug, include_debug,
} => { } => {
let rows = client let rows = client.get_pad_shape_as_polygon(pad_ids.clone(), layer_id)?;
.get_pad_shape_as_polygon(pad_ids.clone(), layer_id)
.await?;
println!( println!(
"pad_shape_total={} layer_id={} requested_pad_count={}", "pad_shape_total={} layer_id={} requested_pad_count={}",
rows.len(), rows.len(),
@ -779,11 +756,9 @@ async fn run() -> Result<(), KiCadError> {
); );
} }
if include_debug { if include_debug {
let raw_chunks = client let raw_chunks = client.get_pad_shape_as_polygon_raw(pad_ids, layer_id)?;
.get_pad_shape_as_polygon_raw(pad_ids, layer_id)
.await?;
for (chunk_index, chunk) in raw_chunks.iter().enumerate() { 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('\n', "\\n")
.replace('\t', " "); .replace('\t', " ");
println!("raw_chunk={chunk_index} debug={debug}"); println!("raw_chunk={chunk_index} debug={debug}");
@ -795,9 +770,8 @@ async fn run() -> Result<(), KiCadError> {
layer_ids, layer_ids,
include_debug, include_debug,
} => { } => {
let rows = client let rows =
.check_padstack_presence_on_layers(item_ids.clone(), layer_ids.clone()) client.check_padstack_presence_on_layers(item_ids.clone(), layer_ids.clone())?;
.await?;
println!( println!(
"padstack_presence_total={} requested_item_count={} requested_layer_count={}", "padstack_presence_total={} requested_item_count={} requested_layer_count={}",
rows.len(), rows.len(),
@ -811,11 +785,10 @@ async fn run() -> Result<(), KiCadError> {
); );
} }
if include_debug { if include_debug {
let raw_chunks = client let raw_chunks =
.check_padstack_presence_on_layers_raw(item_ids, layer_ids) client.check_padstack_presence_on_layers_raw(item_ids, layer_ids)?;
.await?;
for (chunk_index, chunk) in raw_chunks.iter().enumerate() { 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('\n', "\\n")
.replace('\t', " "); .replace('\t', " ");
println!("raw_chunk={chunk_index} debug={debug}"); println!("raw_chunk={chunk_index} debug={debug}");
@ -823,7 +796,7 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::TitleBlock => { 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!("title={}", title_block.title);
println!("date={}", title_block.date); println!("date={}", title_block.date);
println!("revision={}", title_block.revision); println!("revision={}", title_block.revision);
@ -833,28 +806,28 @@ async fn run() -> Result<(), KiCadError> {
} }
} }
Command::BoardAsString => { Command::BoardAsString => {
let content = client.get_board_as_string().await?; let content = client.get_board_as_string()?;
println!("{content}"); println!("{content}");
} }
Command::SelectionAsString => { Command::SelectionAsString => {
let content = client.get_selection_as_string().await?; let content = client.get_selection_as_string()?;
println!("{content}"); println!("{content}");
} }
Command::Stackup => { Command::Stackup => {
let stackup = client.get_board_stackup().await?; let stackup = client.get_board_stackup()?;
println!("{stackup:#?}"); println!("{stackup:#?}");
} }
Command::UpdateStackup => { Command::UpdateStackup => {
let stackup = client.get_board_stackup().await?; let stackup = client.get_board_stackup()?;
let updated = client.update_board_stackup(stackup).await?; let updated = client.update_board_stackup(stackup)?;
println!("{updated:#?}"); println!("{updated:#?}");
} }
Command::GraphicsDefaults => { Command::GraphicsDefaults => {
let defaults = client.get_graphics_defaults().await?; let defaults = client.get_graphics_defaults()?;
println!("{defaults:#?}"); println!("{defaults:#?}");
} }
Command::Appearance => { Command::Appearance => {
let appearance = client.get_board_editor_appearance_settings().await?; let appearance = client.get_board_editor_appearance_settings()?;
println!("{appearance:#?}"); println!("{appearance:#?}");
} }
Command::SetAppearance { Command::SetAppearance {
@ -863,31 +836,31 @@ async fn run() -> Result<(), KiCadError> {
board_flip, board_flip,
ratsnest_display, ratsnest_display,
} => { } => {
let updated = client let updated = client.set_board_editor_appearance_settings(
.set_board_editor_appearance_settings(kicad_ipc::BoardEditorAppearanceSettings { kicad_ipc_rs::BoardEditorAppearanceSettings {
inactive_layer_display, inactive_layer_display,
net_color_display, net_color_display,
board_flip, board_flip,
ratsnest_display, ratsnest_display,
}) },
.await?; )?;
println!("{updated:#?}"); println!("{updated:#?}");
} }
Command::RefillZones { zone_ids } => { Command::RefillZones { zone_ids } => {
client.refill_zones(zone_ids).await?; client.refill_zones(zone_ids)?;
println!("refill_zones_dispatched=ok"); println!("refill_zones_dispatched=ok");
} }
Command::InteractiveMoveItems { item_ids } => { 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()); println!("interactive_move_item_count={}", item_ids.len());
} }
Command::NetClass => { Command::NetClass => {
let nets = client.get_nets().await?; let nets = client.get_nets()?;
let netclasses = client.get_netclass_for_nets(nets).await?; let netclasses = client.get_netclass_for_nets(nets)?;
println!("{netclasses:#?}"); println!("{netclasses:#?}");
} }
Command::BoardReadReport { output } => { 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 { fs::write(&output, report).map_err(|err| KiCadError::Config {
reason: format!("failed to write report to `{}`: {err}", output.display()), reason: format!("failed to write report to `{}`: {err}", output.display()),
})?; })?;
@ -897,9 +870,9 @@ async fn run() -> Result<(), KiCadError> {
print_proto_coverage_board_read(); print_proto_coverage_board_read();
} }
Command::Smoke => { Command::Smoke => {
client.ping().await?; client.ping()?;
let version = client.get_version().await?; let version = client.get_version()?;
let has_board = client.has_open_board().await?; let has_board = client.has_open_board()?;
println!( println!(
"smoke ok: version={}.{}.{} board_open={}", "smoke ok: version={}.{}.{} board_open={}",
version.major, version.minor, version.patch, has_board version.major, version.minor, version.patch, has_board
@ -2189,13 +2162,13 @@ TYPES:
); );
} }
async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String, KiCadError> { fn build_board_read_report_markdown(client: &KiCadClientBlocking) -> Result<String, KiCadError> {
let mut out = String::new(); let mut out = String::new();
out.push_str("# Board Read Reconstruction Report\n\n"); 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("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"); 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("## Session\n\n");
out.push_str(&format!( out.push_str(&format!(
"- KiCad version: {}.{}.{} ({})\n", "- KiCad version: {}.{}.{} ({})\n",
@ -2208,7 +2181,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
)); ));
out.push_str("## Open Documents\n\n"); out.push_str("## Open Documents\n\n");
let docs = client.get_open_documents(DocumentType::Pcb).await?; let docs = client.get_open_documents(DocumentType::Pcb)?;
if docs.is_empty() { if docs.is_empty() {
out.push_str("- No open PCB docs\n\n"); out.push_str("- No open PCB docs\n\n");
} else { } else {
@ -2230,7 +2203,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
} }
out.push_str("## Layer / Origin / Nets\n\n"); out.push_str("## Layer / Origin / Nets\n\n");
let enabled = client.get_board_enabled_layers().await?; let enabled = client.get_board_enabled_layers()?;
let enabled_layers = enabled.layers.clone(); let enabled_layers = enabled.layers.clone();
out.push_str(&format!( out.push_str(&format!(
"- copper_layer_count: {}\n", "- copper_layer_count: {}\n",
@ -2241,34 +2214,30 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id)); out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
} }
let visible_layers = client.get_visible_layers().await?; let visible_layers = client.get_visible_layers()?;
out.push_str("- visible_layers:\n"); out.push_str("- visible_layers:\n");
for layer in visible_layers { for layer in visible_layers {
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id)); out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
} }
let active_layer = client.get_active_layer().await?; let active_layer = client.get_active_layer()?;
out.push_str(&format!( out.push_str(&format!(
"- active_layer: {} ({})\n", "- active_layer: {} ({})\n",
active_layer.name, active_layer.id active_layer.name, active_layer.id
)); ));
let grid_origin = client let grid_origin = client.get_board_origin(kicad_ipc_rs::BoardOriginKind::Grid)?;
.get_board_origin(kicad_ipc::BoardOriginKind::Grid)
.await?;
out.push_str(&format!( out.push_str(&format!(
"- grid_origin_nm: {},{}\n", "- grid_origin_nm: {},{}\n",
grid_origin.x_nm, grid_origin.y_nm grid_origin.x_nm, grid_origin.y_nm
)); ));
let drill_origin = client let drill_origin = client.get_board_origin(kicad_ipc_rs::BoardOriginKind::Drill)?;
.get_board_origin(kicad_ipc::BoardOriginKind::Drill)
.await?;
out.push_str(&format!( out.push_str(&format!(
"- drill_origin_nm: {},{}\n", "- drill_origin_nm: {},{}\n",
drill_origin.x_nm, drill_origin.y_nm drill_origin.x_nm, drill_origin.y_nm
)); ));
let nets = client.get_nets().await?; let nets = client.get_nets()?;
out.push_str(&format!("- net_count: {}\n", nets.len())); out.push_str(&format!("- net_count: {}\n", nets.len()));
out.push_str("\n### Netlist\n\n"); out.push_str("\n### Netlist\n\n");
for net in &nets { for net in &nets {
@ -2277,7 +2246,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
out.push('\n'); out.push('\n');
out.push_str("### Pad-Level Netlist (Footprint/Pad/Net)\n\n"); out.push_str("### Pad-Level Netlist (Footprint/Pad/Net)\n\n");
let pad_entries = client.get_pad_netlist().await?; let pad_entries = client.get_pad_netlist()?;
let mut pad_ids = BTreeSet::new(); let mut pad_ids = BTreeSet::new();
out.push_str(&format!("- pad_entry_count: {}\n", pad_entries.len())); out.push_str(&format!("- pad_entry_count: {}\n", pad_entries.len()));
for (index, entry) in pad_entries.iter().enumerate() { for (index, entry) in pad_entries.iter().enumerate() {
@ -2319,9 +2288,8 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
)); ));
let mut present_pad_ids_by_layer: BTreeMap<i32, BTreeSet<String>> = BTreeMap::new(); let mut present_pad_ids_by_layer: BTreeMap<i32, BTreeSet<String>> = BTreeMap::new();
let presence_rows = client let presence_rows =
.check_padstack_presence_on_layers(pad_ids.clone(), enabled_layer_ids) client.check_padstack_presence_on_layers(pad_ids.clone(), enabled_layer_ids)?;
.await?;
out.push_str(&format!( out.push_str(&format!(
"- presence_entry_count: {}\n", "- presence_entry_count: {}\n",
presence_rows.len() presence_rows.len()
@ -2372,9 +2340,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
continue; continue;
} }
let polygons = client let polygons = client.get_pad_shape_as_polygon(pad_ids_on_layer, layer.id)?;
.get_pad_shape_as_polygon(pad_ids_on_layer, layer.id)
.await?;
out.push_str(&format!("- polygon_entry_count: {}\n\n", polygons.len())); out.push_str(&format!("- polygon_entry_count: {}\n\n", polygons.len()));
for row in polygons { for row in polygons {
let summary = polygon_geometry_summary(&row.polygon); let summary = polygon_geometry_summary(&row.polygon);
@ -2395,7 +2361,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
out.push_str("## Board/Editor Structures\n\n"); out.push_str("## Board/Editor Structures\n\n");
out.push_str("### Title Block\n\n"); out.push_str("### Title Block\n\n");
let title_block = client.get_title_block_info().await?; let title_block = client.get_title_block_info()?;
out.push_str(&format!("- title: {}\n", title_block.title)); out.push_str(&format!("- title: {}\n", title_block.title));
out.push_str(&format!("- date: {}\n", title_block.date)); out.push_str(&format!("- date: {}\n", title_block.date));
out.push_str(&format!("- revision: {}\n", title_block.revision)); out.push_str(&format!("- revision: {}\n", title_block.revision));
@ -2406,40 +2372,35 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
out.push('\n'); out.push('\n');
out.push_str("### Stackup\n\n```text\n"); out.push_str("### Stackup\n\n```text\n");
out.push_str(&format!("{:#?}", client.get_board_stackup().await?)); out.push_str(&format!("{:#?}", client.get_board_stackup()?));
out.push_str("\n```\n\n"); out.push_str("\n```\n\n");
out.push_str("### Graphics Defaults\n\n```text\n"); out.push_str("### Graphics Defaults\n\n```text\n");
out.push_str(&format!("{:#?}", client.get_graphics_defaults().await?)); out.push_str(&format!("{:#?}", client.get_graphics_defaults()?));
out.push_str("\n```\n\n"); out.push_str("\n```\n\n");
out.push_str("### Editor Appearance\n\n```text\n"); out.push_str("### Editor Appearance\n\n```text\n");
out.push_str(&format!( out.push_str(&format!(
"{:#?}", "{:#?}",
client.get_board_editor_appearance_settings().await? client.get_board_editor_appearance_settings()?
)); ));
out.push_str("\n```\n\n"); out.push_str("\n```\n\n");
out.push_str("### NetClass Map\n\n```text\n"); out.push_str("### NetClass Map\n\n```text\n");
out.push_str(&format!( out.push_str(&format!(
"{:#?}", "{:#?}",
client client.get_netclass_for_nets(client.get_nets()?)?
.get_netclass_for_nets(client.get_nets().await?)
.await?
)); ));
out.push_str("\n```\n\n"); out.push_str("\n```\n\n");
out.push_str("## PCB Item Coverage (All KOT_PCB_* Types)\n\n"); out.push_str("## PCB Item Coverage (All KOT_PCB_* Types)\n\n");
let mut missing_types: Vec<PcbObjectTypeCode> = Vec::new(); let mut missing_types: Vec<PcbObjectTypeCode> = 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!( out.push_str(&format!(
"### {} ({})\n\n", "### {} ({})\n\n",
object_type.name, object_type.code object_type.name, object_type.code
)); ));
match client match client.get_items_raw_by_type_codes(vec![object_type.code]) {
.get_items_raw_by_type_codes(vec![object_type.code])
.await
{
Ok(items) => { Ok(items) => {
if items.is_empty() { if items.is_empty() {
missing_types.push(*object_type); missing_types.push(*object_type);
@ -2451,7 +2412,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
.take(REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE) .take(REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE)
.enumerate() .enumerate()
{ {
let mut debug = kicad_ipc::KiCadClient::debug_any_item(item)?; let mut debug = kicad_ipc_rs::KiCadClient::debug_any_item(item)?;
if debug.len() > REPORT_MAX_ITEM_DEBUG_CHARS { if debug.len() > REPORT_MAX_ITEM_DEBUG_CHARS {
debug.truncate(REPORT_MAX_ITEM_DEBUG_CHARS); debug.truncate(REPORT_MAX_ITEM_DEBUG_CHARS);
debug.push_str("\n...<truncated; use items-raw CLI for full payload>"); debug.push_str("\n...<truncated; use items-raw CLI for full payload>");
@ -2495,7 +2456,7 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
} }
out.push_str("## Board File Snapshot (Raw)\n\n```scheme\n"); out.push_str("## Board File Snapshot (Raw)\n\n```scheme\n");
let mut board_text = client.get_board_as_string().await?; let mut board_text = client.get_board_as_string()?;
if board_text.len() > REPORT_MAX_BOARD_SNAPSHOT_CHARS { if board_text.len() > REPORT_MAX_BOARD_SNAPSHOT_CHARS {
board_text.truncate(REPORT_MAX_BOARD_SNAPSHOT_CHARS); board_text.truncate(REPORT_MAX_BOARD_SNAPSHOT_CHARS);
board_text.push_str( board_text.push_str(
@ -2665,7 +2626,7 @@ struct PolygonGeometrySummary {
arc_nodes: usize, 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 { let mut summary = PolygonGeometrySummary {
hole_count: polygon.holes.len(), hole_count: polygon.holes.len(),
..PolygonGeometrySummary::default() ..PolygonGeometrySummary::default()
@ -2675,8 +2636,8 @@ fn polygon_geometry_summary(polygon: &kicad_ipc::PolygonWithHolesNm) -> PolygonG
summary.outline_nodes = outline.nodes.len(); summary.outline_nodes = outline.nodes.len();
for node in &outline.nodes { for node in &outline.nodes {
match node { match node {
kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, kicad_ipc_rs::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1,
kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_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(); summary.hole_nodes_total += hole.nodes.len();
for node in &hole.nodes { for node in &hole.nodes {
match node { match node {
kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, kicad_ipc_rs::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1,
kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1, kicad_ipc_rs::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1,
} }
} }
} }
@ -2765,7 +2726,7 @@ fn hex_nibble(c: char) -> Result<u8, String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{parse_args_from, Command}; use super::{parse_args_from, Command};
use kicad_ipc::{ use kicad_ipc_rs::{
BoardFlipMode, BoardOriginKind, CommitAction, DrcSeverity, InactiveLayerDisplayMode, BoardFlipMode, BoardOriginKind, CommitAction, DrcSeverity, InactiveLayerDisplayMode,
NetColorDisplayMode, RatsnestDisplayMode, NetColorDisplayMode, RatsnestDisplayMode,
}; };
@ -2930,7 +2891,7 @@ mod tests {
match command { match command {
Command::SetNetClasses { merge_mode } => { 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:?}"), other => panic!("unexpected command variant: {other:?}"),
} }
@ -2952,7 +2913,7 @@ mod tests {
merge_mode, merge_mode,
variables, 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")); assert_eq!(variables.get("REV").map(|value| value.as_str()), Some("A"));
} }
other => panic!("unexpected command variant: {other:?}"), other => panic!("unexpected command variant: {other:?}"),