use nng::{Protocol, Socket};
use num_traits::{FromPrimitive, Num, ToPrimitive};
use protobuf::well_known_types::any::Any;
use protobuf::{EnumOrUnknown, Message, MessageFull};
use rand::distr::{Alphanumeric, SampleString};
use std::cell::RefCell;
use std::env;
use std::fmt::Display;
#[macro_use]
extern crate quick_error;
pub mod board;
mod protos;
mod api_version;
use crate::board::Board;
use crate::protos::base_commands::*;
use crate::protos::base_types::DocumentSpecifier;
use crate::protos::editor_commands::*;
use crate::protos::envelope::*;
pub use crate::protos::base_types::DocumentType;
pub use crate::protos::board_types::BoardLayer;
pub use api_version::*;
quick_error! {
#[derive(Debug)]
pub enum KiCadError {
ConnectionFailed(err: nng::Error) {
display("could not connect to KiCad: {}", err)
from()
from(e: (nng::Message, nng::Error)) -> (e.1)
}
ProtocolError(err: protobuf::Error) {
display("could not decode message: {}", err)
from()
}
ApiError(msg: String) {
from()
}
}
}
type KiCadResult<T> = Result<T, KiCadError>;
#[derive(Debug)]
pub struct KiCad {
socket: Box<Socket>,
config: RefCell<KiCadConnectionConfig>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct KiCadVersion<'a> {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub full: std::borrow::Cow<'a, str>,
}
impl KiCadVersion<'_> {
pub fn into_owned(self) -> KiCadVersion<'static> {
KiCadVersion {
major: self.major,
minor: self.minor,
patch: self.patch,
full: std::borrow::Cow::Owned(self.full.into_owned()),
}
}
pub fn new(major: u32, minor: u32, patch: u32, full: String) -> KiCadVersion<'static> {
KiCadVersion {
major,
minor,
patch,
full: full.into(),
}
}
}
impl Display for KiCadVersion<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.full)
}
}
impl From<protos::base_types::KiCadVersion> for KiCadVersion<'_> {
fn from(value: protos::base_types::KiCadVersion) -> Self {
Self {
major: value.major,
minor: value.minor,
patch: value.patch,
full: value.full_version.into(),
}
}
}
impl<'a> From<&'a protos::base_types::KiCadVersion> for KiCadVersion<'a> {
fn from(value: &'a protos::base_types::KiCadVersion) -> Self {
Self {
major: value.major,
minor: value.minor,
patch: value.patch,
full: (&value.full_version).into(),
}
}
}
type Coord = i64;
pub fn to_mm(iu: Coord) -> f64 {
iu as f64 / 1_000_000f64
}
pub fn from_mm<F: Num + ToPrimitive + FromPrimitive>(x: F) -> Coord {
(x * F::from_f64(1_000_000f64).unwrap()).to_i64().unwrap()
}
#[derive(Debug)]
pub struct KiCadConnectionConfig {
pub socket_path: String,
pub client_name: String,
pub kicad_token: String,
}
impl Default for KiCadConnectionConfig {
fn default() -> Self {
let socket_path = match env::consts::OS {
"windows" => {
format!(
"ipc://{}\\kicad\\api.sock",
env::temp_dir().to_str().unwrap()
)
}
_ => String::from("ipc:///tmp/kicad/api.sock"),
};
let mut client_name: String = Alphanumeric.sample_string(&mut rand::rng(), 8);
client_name.insert_str(0, "anonymous-");
Self {
socket_path,
client_name,
kicad_token: String::new(),
}
}
}
impl KiCad {
pub fn new(config: KiCadConnectionConfig) -> KiCadResult<KiCad> {
let socket = Socket::new(Protocol::Req0)?;
socket.dial(&config.socket_path)?;
Ok(KiCad {
socket: Box::new(socket),
config: RefCell::new(config),
})
}
fn send_envelope(&self, req: ApiRequest) -> KiCadResult<ApiResponse> {
self.socket.send(req.write_to_bytes()?.as_slice())?;
let response = ApiResponse::parse_from_bytes(self.socket.recv()?.as_slice())?;
match response.status.status.enum_value_or_default() {
ApiStatusCode::AS_OK => {
let mut config = self.config.borrow_mut();
if config.kicad_token.is_empty() {
config.kicad_token = String::from(&response.header.kicad_token);
}
Ok(response)
}
_ => Err(KiCadError::ApiError(format!(
"KiCad API returned error: {}",
response.status.error_message
))),
}
}
fn send_request<T: MessageFull, U: MessageFull>(&self, message: T) -> KiCadResult<U> {
let mut req = ApiRequest::new();
req.header = Some(ApiRequestHeader::new()).into();
let header = req.header.as_mut().unwrap();
{
let config = self.config.borrow();
header.client_name = config.client_name.clone();
header.kicad_token = config.kicad_token.clone();
}
req.message = Some(Any::pack(&message)?).into();
let rep = self.send_envelope(req)?;
let message = Any::unpack::<U>(rep.message.get_or_default())?;
match message {
Some(message) => Ok(message),
None => Err(KiCadError::ApiError(format!(
"could not unpack {} from API response",
U::descriptor().name()
))),
}
}
pub fn get_version(&self) -> KiCadResult<KiCadVersion> {
let reply: GetVersionResponse = self.send_request(GetVersion::new())?;
Ok(reply.version.get_or_default().clone().into())
}
pub fn get_open_documents(
&self,
doc_type: DocumentType,
) -> KiCadResult<Vec<DocumentSpecifier>> {
let mut message = GetOpenDocuments::new();
message.type_ = EnumOrUnknown::from(doc_type);
Ok(self
.send_request::<_, GetOpenDocumentsResponse>(message)?
.documents)
}
pub fn get_board(&self, doc: &DocumentSpecifier) -> KiCadResult<Board> {
Ok(Board {
kicad: self,
doc: doc.clone(),
})
}
pub fn get_open_board(&self) -> KiCadResult<Board> {
let docs = self
.get_open_documents(DocumentType::DOCTYPE_PCB)
.unwrap_or_default();
match docs.first() {
Some(doc) => self.get_board(doc),
_ => Err(KiCadError::ApiError(String::from("no boards are open"))),
}
}
}