1use 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#[derive(Debug)]
75pub struct KiCad {
76 socket: Box<Socket>,
77 config: RefCell<KiCadConnectionConfig>,
78}
79
80#[derive(Debug, Clone, Eq, PartialEq)]
82pub struct KiCadVersion<'a> {
83 pub major: u32,
84 pub minor: u32,
85 pub patch: u32,
86 pub full: std::borrow::Cow<'a, str>,
90}
91
92impl KiCadVersion<'_> {
93 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
139type 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#[derive(Debug)]
152pub struct KiCadConnectionConfig {
153 pub socket_path: String,
157
158 pub client_name: String,
162
163 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}