app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
Colby Knox
Restore 25 files to their state before PR #61: the wiki's pr revert left them truncated to the diff's context
4f3a0f4
8d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
//! A SPICE simulation through the ngspice shared library KiCad installs (`<kicad>/bin/ngspice.dll`), the
//! engine behind KiCad's own simulator (wiki #89). ngspice keeps global state and can bring its host down
//! on a bad circuit, so the bridge runs this in a child process of itself (`kicad-bridge --ngspice-worker`)
//! and only ever once per process. Windows only for now: the library names its API in sharedspice.h.
use serde_json::{json, Value};
/// Run `lines` (a whole circuit, analysis lines and `.end` included) in the ngspice library at `dll`, with
/// `cwd` as the working folder (so `.include` paths resolve), and return every vector of the last plot:
/// `{plot, vectors: [{name, type, real: [..]} | {name, type, re: [..], im: [..]}], log: [..]}`.
/// `codemodels` are the XSPICE code model libraries (`<kicad>/lib/ngspice/*.cm`) loaded first, as KiCad's own
/// simulator does: KiCad ships no spinit to load them.
/// `commands` run after the circuit is loaded and before `run` (a sweep's `alter R4 = 300`); a command ngspice
/// reports an error for fails the run.
pub fn run(dll: &std::path::Path, lines: &[String], cwd: &std::path::Path, codemodels: &[std::path::PathBuf], commands: &[String]) -> Result<Value, String> {
imp::run(dll, lines, cwd, codemodels, commands)
}
#[cfg(windows)]
mod imp {
use super::*;
use std::ffi::{c_char, c_int, c_void, CStr, CString};
use std::sync::Mutex;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HMODULE;
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryExW, LOAD_WITH_ALTERED_SEARCH_PATH};
#[repr(C)]
struct Complex {
re: f64,
im: f64,
}
#[repr(C)]
struct VectorInfo {
name: *const c_char,
v_type: c_int,
flags: i16,
real: *const f64,
comp: *const Complex,
length: c_int,
}
type SendChar = extern "C" fn(*const c_char, c_int, *mut c_void) -> c_int;
type SendStat = extern "C" fn(*const c_char, c_int, *mut c_void) -> c_int;
type Exit = extern "C" fn(c_int, bool, bool, c_int, *mut c_void) -> c_int;
type Init = unsafe extern "C" fn(Option<SendChar>, Option<SendStat>, Option<Exit>, *mut c_void, *mut c_void, *mut c_void, *mut c_void) -> c_int;
type Circ = unsafe extern "C" fn(*mut *mut c_char) -> c_int;
type Command = unsafe extern "C" fn(*const c_char) -> c_int;
type CurPlot = unsafe extern "C" fn() -> *const c_char;
type AllVecs = unsafe extern "C" fn(*const c_char) -> *mut *mut c_char;
type VecInfo = unsafe extern "C" fn(*const c_char) -> *const VectorInfo;
static LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
extern "C" fn on_print(s: *const c_char, _id: c_int, _u: *mut c_void) -> c_int {
if !s.is_null() {
let t = unsafe { CStr::from_ptr(s) }.to_string_lossy().into_owned();
if let Ok(mut l) = LOG.lock() {
if l.len() < 400 {
l.push(t);
}
}
}
0
}
extern "C" fn on_stat(_s: *const c_char, _id: c_int, _u: *mut c_void) -> c_int {
0
}
extern "C" fn on_exit(_status: c_int, _unload: bool, _quit: bool, _id: c_int, _u: *mut c_void) -> c_int {
0
}
fn sym<T: Copy>(lib: HMODULE, name: &str) -> Result<T, String> {
let c = CString::new(name).unwrap();
let p = unsafe { GetProcAddress(lib, windows::core::PCSTR(c.as_ptr() as *const u8)) }.ok_or(format!("ngspice has no {name}"))?;
Ok(unsafe { std::mem::transmute_copy(&p) })
}
pub fn run(dll: &std::path::Path, lines: &[String], cwd: &std::path::Path, codemodels: &[std::path::PathBuf], commands: &[String]) -> Result<Value, String> {
std::env::set_current_dir(cwd).map_err(|e| format!("cannot work in {}: {e}", cwd.display()))?;
let wide: Vec<u16> = dll.as_os_str().to_string_lossy().encode_utf16().chain(std::iter::once(0)).collect();
// the altered search path finds the library's own dependencies beside it, in KiCad's bin folder
let lib = unsafe { LoadLibraryExW(PCWSTR(wide.as_ptr()), None, LOAD_WITH_ALTERED_SEARCH_PATH) }.map_err(|e| format!("cannot load {}: {e}", dll.display()))?;
let init: Init = sym(lib, "ngSpice_Init")?;
let circ: Circ = sym(lib, "ngSpice_Circ")?;
let command: Command = sym(lib, "ngSpice_Command")?;
let cur_plot: CurPlot = sym(lib, "ngSpice_CurPlot")?;
let all_vecs: AllVecs = sym(lib, "ngSpice_AllVecs")?;
let vec_info: VecInfo = sym(lib, "ngGet_Vec_Info")?;
let null = std::ptr::null_mut();
unsafe { init(Some(on_print), Some(on_stat), Some(on_exit), null, null, null, null) };
for cm in codemodels {
let c = CString::new(format!("codemodel {}", cm.to_string_lossy().replace('\\', "/"))).unwrap();
unsafe { command(c.as_ptr()) };
}
let owned: Vec<CString> = lines.iter().map(|l| CString::new(l.replace('\0', "")).unwrap()).collect();
let mut ptrs: Vec<*mut c_char> = owned.iter().map(|c| c.as_ptr() as *mut c_char).collect();
ptrs.push(std::ptr::null_mut());
if unsafe { circ(ptrs.as_mut_ptr()) } != 0 {
return Err(format!("ngspice did not take the circuit: {}", errors()));
}
for c in commands {
let seen = LOG.lock().map(|l| l.len()).unwrap_or(0);
let cs = CString::new(c.replace('\0', "")).unwrap();
let rc = unsafe { command(cs.as_ptr()) };
let new: Vec<String> = LOG.lock().map(|l| l.iter().skip(seen).cloned().collect()).unwrap_or_default();
if let Some(e) = new.iter().find(|l| { let t = l.to_lowercase(); t.contains("error") || t.contains("no such") }) {
return Err(format!("ngspice refused \"{c}\": {e}"));
}
if rc != 0 {
return Err(format!("ngspice refused \"{c}\": {}", errors()));
}
}
let run = CString::new("run").unwrap();
if unsafe { command(run.as_ptr()) } != 0 {
return Err(format!("the analysis did not run: {}", errors()));
}
let plot_ptr = unsafe { cur_plot() };
let plot = if plot_ptr.is_null() { String::new() } else { unsafe { CStr::from_ptr(plot_ptr) }.to_string_lossy().into_owned() };
// an aborted analysis leaves the constants plot current: that is a failure, not a result
let aborted = LOG.lock().map(|l| l.iter().any(|s| { let t = s.to_lowercase(); t.contains("aborted") || t.contains("fatal error") })).unwrap_or(false);
if aborted || plot.is_empty() || plot == "const" {
return Err(format!("the analysis did not run: {}", errors()));
}
// every vector of one plot by name; a noise analysis leaves two plots (noiseN: the spectra over frequency,
// noiseN+1: the integrated totals), so the other one is read too
let read_plot = |name: &str| -> Vec<Value> {
let Ok(cname) = CString::new(name) else { return Vec::new() };
let mut names = Vec::new();
let list = unsafe { all_vecs(cname.as_ptr()) };
if !list.is_null() {
let mut i = 0;
loop {
let p = unsafe { *list.add(i) };
if p.is_null() {
break;
}
names.push(unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned());
i += 1;
}
}
let mut vectors = Vec::new();
for n in &names {
let full = CString::new(format!("{name}.{n}")).unwrap();
let info = unsafe { vec_info(full.as_ptr()) };
if info.is_null() {
continue;
}
let v = unsafe { &*info };
let len = v.length.max(0) as usize;
if !v.real.is_null() {
let data = unsafe { std::slice::from_raw_parts(v.real, len) };
vectors.push(json!({"name": n, "type": v.v_type, "real": data}));
} else if !v.comp.is_null() {
let data = unsafe { std::slice::from_raw_parts(v.comp, len) };
vectors.push(json!({"name": n, "type": v.v_type, "re": data.iter().map(|c| c.re).collect::<Vec<_>>(), "im": data.iter().map(|c| c.im).collect::<Vec<_>>()}));
}
}
vectors
};
let mut vectors = read_plot(&plot);
let mut plot = plot;
if let Some(n) = plot.strip_prefix("noise").and_then(|d| d.parse::<u32>().ok()) {
let (spectra, totals) = if vectors.iter().any(|v| v["name"] == "frequency") { (n, n + 1) } else { (n.saturating_sub(1), n) };
let other = if spectra == n { totals } else { spectra };
let names: Vec<Value> = vectors.iter().map(|v| v["name"].clone()).collect();
for v in read_plot(&format!("noise{other}")) {
if !names.contains(&v["name"]) {
vectors.push(v);
}
}
// the spectra plot leads: its frequency is the sweep
plot = format!("noise{spectra}");
}
if vectors.is_empty() {
return Err(format!("the analysis gave no vectors: {}", errors()));
}
Ok(json!({"plot": plot, "vectors": vectors, "log": LOG.lock().map(|l| l.clone()).unwrap_or_default()}))
}
fn errors() -> String {
let l = LOG.lock().map(|l| l.clone()).unwrap_or_default();
let e: Vec<&String> = l.iter().filter(|s| { let t = s.to_lowercase(); (t.contains("error") || t.contains("aborted")) && !t.contains("spinit") }).take(8).collect();
if e.is_empty() { l.iter().rev().take(6).cloned().collect::<Vec<_>>().join(" | ") } else { e.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" | ") }
}
}
#[cfg(not(windows))]
mod imp {
use super::*;
pub fn run(_dll: &std::path::Path, _lines: &[String], _cwd: &std::path::Path, _codemodels: &[std::path::PathBuf], _commands: &[String]) -> Result<Value, String> {
Err("simulation runs through KiCad's ngspice.dll, on Windows for now".into())
}
}