kicad/
lib.rs

1// This program source code file is part of KiCad, a free EDA CAD application.
2//
3// Copyright (C) 2024 KiCad Developers
4//
5// This program is free software: you can redistribute it and/or modify it
6// under the terms of the GNU General Public License as published by the
7// Free Software Foundation, either version 3 of the License, or (at your
8// option) any later version.
9//
10// This program is distributed in the hope that it will be useful, but
11// WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License along
16// with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18//! # KiCad IPC API Bindings
19//!
20//! `kicad-api-rs` is a library providing bindings to the KiCad IPC API, which allows external
21//! applications to communicate with a running instance of KiCad.  This can be used to build
22//! plugins that are launched by KiCad, but also to enable standalone applications to interface
23//! with KiCad.
24
25use nng::{Protocol, Socket};
26use num_traits::{FromPrimitive, Num, ToPrimitive};
27use protobuf::well_known_types::any::Any;
28use protobuf::{EnumOrUnknown, Message, MessageFull};
29use rand::distr::{Alphanumeric, SampleString};
30use std::cell::RefCell;
31use std::env;
32use std::fmt::Display;
33
34#[macro_use]
35extern crate quick_error;
36
37pub mod board;
38mod protos;
39
40mod api_version;
41
42use crate::board::Board;
43use crate::protos::base_commands::*;
44use crate::protos::base_types::DocumentSpecifier;
45use crate::protos::editor_commands::*;
46use crate::protos::envelope::*;
47
48pub use crate::protos::base_types::DocumentType;
49pub use crate::protos::board_types::BoardLayer;
50
51pub use api_version::*;
52
53quick_error! {
54    #[derive(Debug)]
55    pub enum KiCadError {
56        ConnectionFailed(err: nng::Error) {
57            display("could not connect to KiCad: {}", err)
58            from()
59            from(e: (nng::Message, nng::Error)) -> (e.1)
60        }
61        ProtocolError(err: protobuf::Error) {
62            display("could not decode message: {}", err)
63            from()
64        }
65        ApiError(msg: String) {
66            from()
67        }
68    }
69}
70
71type KiCadResult<T> = Result<T, KiCadError>;
72
73/// Represents a connection to KiCad and its top-level API calls.
74#[derive(Debug)]
75pub struct KiCad {
76    socket: Box<Socket>,
77    config: RefCell<KiCadConnectionConfig>,
78}
79
80/// Describes a particular version of KiCad
81#[derive(Debug, Clone, Eq, PartialEq)]
82pub struct KiCadVersion<'a> {
83    pub major: u32,
84    pub minor: u32,
85    pub patch: u32,
86    /// Full version string.
87    ///
88    /// Can include additional information like release candidate specifiers and commit hashes.
89    pub full: std::borrow::Cow<'a, str>,
90}
91
92impl KiCadVersion<'_> {
93    /// Turn this version into a owned one, cloning the version string if its not already owned.
94    pub fn into_owned(self) -> KiCadVersion<'static> {
95        KiCadVersion {
96            major: self.major,
97            minor: self.minor,
98            patch: self.patch,
99            full: std::borrow::Cow::Owned(self.full.into_owned()),
100        }
101    }
102    pub fn new(major: u32, minor: u32, patch: u32, full: String) -> KiCadVersion<'static> {
103        KiCadVersion {
104            major,
105            minor,
106            patch,
107            full: full.into(),
108        }
109    }
110}
111
112impl Display for KiCadVersion<'_> {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}", self.full)
115    }
116}
117
118impl From<protos::base_types::KiCadVersion> for KiCadVersion<'_> {
119    fn from(value: protos::base_types::KiCadVersion) -> Self {
120        Self {
121            major: value.major,
122            minor: value.minor,
123            patch: value.patch,
124            full: value.full_version.into(),
125        }
126    }
127}
128impl<'a> From<&'a protos::base_types::KiCadVersion> for KiCadVersion<'a> {
129    fn from(value: &'a protos::base_types::KiCadVersion) -> Self {
130        Self {
131            major: value.major,
132            minor: value.minor,
133            patch: value.patch,
134            full: (&value.full_version).into(),
135        }
136    }
137}
138
139// KiCad uses 64-bit nanometers for all measurements in the API
140type Coord = i64;
141
142pub fn to_mm(iu: Coord) -> f64 {
143    iu as f64 / 1_000_000f64
144}
145
146pub fn from_mm<F: Num + ToPrimitive + FromPrimitive>(x: F) -> Coord {
147    (x * F::from_f64(1_000_000f64).unwrap()).to_i64().unwrap()
148}
149
150/// Configuration options passed to KiCad::new()
151#[derive(Debug)]
152pub struct KiCadConnectionConfig {
153    /// The path to KiCad's IPC server socket.  Leave default to use the platform-dependent
154    /// default path.  KiCad will provide this value in an environment variable when launching
155    /// API plugins.
156    pub socket_path: String,
157
158    /// The name of this API client.  Leave default to generate a random client name.  This name
159    /// should uniquely identify a running instance of the client, especially if the user may
160    /// launch more than one instance of the client at a time.
161    pub client_name: String,
162
163    /// A token identifying a running instance of KiCad.  Leave default to not specify a KiCad
164    /// instance.  The first command sent to KiCad will include that KiCad instance's token in the
165    /// response, which should then be used on subsequent commands to ensure the client can detect
166    /// if a different KiCad instance is responding (for example, if KiCad is closed and
167    /// re-opened by the user).
168    pub kicad_token: String,
169}
170
171impl Default for KiCadConnectionConfig {
172    fn default() -> Self {
173        let socket_path = match env::consts::OS {
174            "windows" => {
175                format!(
176                    "ipc://{}\\kicad\\api.sock",
177                    env::temp_dir().to_str().unwrap()
178                )
179            }
180            _ => String::from("ipc:///tmp/kicad/api.sock"),
181        };
182
183        let mut client_name: String = Alphanumeric.sample_string(&mut rand::rng(), 8);
184        client_name.insert_str(0, "anonymous-");
185
186        Self {
187            socket_path,
188            client_name,
189            kicad_token: String::new(),
190        }
191    }
192}
193
194impl KiCad {
195    pub fn new(config: KiCadConnectionConfig) -> KiCadResult<KiCad> {
196        let socket = Socket::new(Protocol::Req0)?;
197        socket.dial(&config.socket_path)?;
198
199        Ok(KiCad {
200            socket: Box::new(socket),
201            config: RefCell::new(config),
202        })
203    }
204
205    fn send_envelope(&self, req: ApiRequest) -> KiCadResult<ApiResponse> {
206        self.socket.send(req.write_to_bytes()?.as_slice())?;
207        let response = ApiResponse::parse_from_bytes(self.socket.recv()?.as_slice())?;
208
209        match response.status.status.enum_value_or_default() {
210            ApiStatusCode::AS_OK => {
211                let mut config = self.config.borrow_mut();
212
213                if config.kicad_token.is_empty() {
214                    config.kicad_token = String::from(&response.header.kicad_token);
215                }
216
217                Ok(response)
218            }
219            _ => Err(KiCadError::ApiError(format!(
220                "KiCad API returned error: {}",
221                response.status.error_message
222            ))),
223        }
224    }
225
226    fn send_request<T: MessageFull, U: MessageFull>(&self, message: T) -> KiCadResult<U> {
227        let mut req = ApiRequest::new();
228
229        req.header = Some(ApiRequestHeader::new()).into();
230        let header = req.header.as_mut().unwrap();
231
232        {
233            let config = self.config.borrow();
234            header.client_name = config.client_name.clone();
235            header.kicad_token = config.kicad_token.clone();
236        }
237
238        req.message = Some(Any::pack(&message)?).into();
239        let rep = self.send_envelope(req)?;
240        let message = Any::unpack::<U>(rep.message.get_or_default())?;
241        match message {
242            Some(message) => Ok(message),
243            None => Err(KiCadError::ApiError(format!(
244                "could not unpack {} from API response",
245                U::descriptor().name()
246            ))),
247        }
248    }
249
250    pub fn get_version(&self) -> KiCadResult<KiCadVersion> {
251        let reply: GetVersionResponse = self.send_request(GetVersion::new())?;
252        Ok(reply.version.get_or_default().clone().into())
253    }
254
255    pub fn get_open_documents(
256        &self,
257        doc_type: DocumentType,
258    ) -> KiCadResult<Vec<DocumentSpecifier>> {
259        let mut message = GetOpenDocuments::new();
260        message.type_ = EnumOrUnknown::from(doc_type);
261        Ok(self
262            .send_request::<_, GetOpenDocumentsResponse>(message)?
263            .documents)
264    }
265
266    pub fn get_board(&self, doc: &DocumentSpecifier) -> KiCadResult<Board> {
267        Ok(Board {
268            kicad: self,
269            doc: doc.clone(),
270        })
271    }
272
273    pub fn get_open_board(&self) -> KiCadResult<Board> {
274        let docs = self
275            .get_open_documents(DocumentType::DOCTYPE_PCB)
276            .unwrap_or_default();
277
278        match docs.first() {
279            Some(doc) => self.get_board(doc),
280            _ => Err(KiCadError::ApiError(String::from("no boards are open"))),
281        }
282    }
283}