diff --git a/tools/kicad/kicad_functions/Cargo.toml b/tools/kicad/kicad_functions/Cargo.toml index 3873845..a1e71ba 100644 --- a/tools/kicad/kicad_functions/Cargo.toml +++ b/tools/kicad/kicad_functions/Cargo.toml @@ -8,4 +8,5 @@ edition = "2018" [dependencies] evalexpr = "6.1" +regex = "1.5" resistor-calc = { git = "https://github.com/racklet/resistor-calc.git", branch = "fix-no-expr_builder-compile", default-features = false } diff --git a/tools/kicad/kicad_functions/src/vdiv.rs b/tools/kicad/kicad_functions/src/vdiv.rs index 8c59931..6905420 100644 --- a/tools/kicad/kicad_functions/src/vdiv.rs +++ b/tools/kicad/kicad_functions/src/vdiv.rs @@ -2,8 +2,11 @@ use crate::util::err; use evalexpr::{ ContextWithMutableVariables, EvalexprError, EvalexprResult, HashMapContext, Node, Value, }; +use regex::Regex; use resistor_calc::{RCalc, RRes, RSeries}; +use std::cell::RefCell; use std::collections::HashSet; +use std::ops::Deref; use std::panic::panic_any; fn parse_series(str: &str) -> EvalexprResult<&'static RSeries> { @@ -18,15 +21,26 @@ fn parse_series(str: &str) -> EvalexprResult<&'static RSeries> { } } -// evalexpr is not smart enough to distinguish identifiers on its own -fn unique_identifiers(e: &Node) -> usize { +fn resistor_identifiers(e: &Node) -> usize { + let re = Regex::new(r"^R[1-9][0-9]*$").unwrap(); let mut set = HashSet::new(); - e.iter_variable_identifiers().for_each(|i| { - set.insert(i); - }); + e.iter_variable_identifiers() + .filter(|i| re.is_match(i)) // Match only R? identifiers + .for_each(|i| { + set.insert(i); + }); set.len() } +// Helper for parsing of potentially single-element tuples +fn parse_tuple(v: &Value) -> Vec { + if let Ok(t) = v.as_tuple() { + return t; + } + + vec![v.clone()] +} + struct VoltageDividerConfig { target: f64, expression: Node, @@ -34,19 +48,30 @@ struct VoltageDividerConfig { series: &'static RSeries, resistance_min: Option, resistance_max: Option, + extra_parameters: Option>, } impl VoltageDividerConfig { fn parse(v: &Value) -> EvalexprResult { let tuple = v.as_tuple()?; - let resistance_min = tuple.get(3).map(|v| v.as_number()).transpose()?; - let resistance_max = tuple.get(4).map(|v| v.as_number()).transpose()?; + let resistance = tuple.get(3).map(|v| v.as_tuple()).transpose()?; + let resistance_min = resistance + .as_ref() + .map(|r| r.get(0).map(|v| v.as_number())) + .flatten() + .transpose()?; + let resistance_max = resistance + .as_ref() + .map(|r| r.get(1).map(|v| v.as_number())) + .flatten() + .transpose()?; + + let extra_parameters = tuple.get(4).map(|v| parse_tuple(v)); - // TODO: Support external constants if let [target, expression, series] = &tuple[..3] { let expression = evalexpr::build_operator_tree(&expression.as_string()?)?; - let count = unique_identifiers(&expression); + let count = resistor_identifiers(&expression); Ok(Self { target: target.as_number()?, @@ -55,6 +80,7 @@ impl VoltageDividerConfig { series: parse_series(&series.as_string()?)?, resistance_min, resistance_max, + extra_parameters, }) } else { err(&format!("unsupported argument count: {}", tuple.len())) @@ -65,6 +91,16 @@ impl VoltageDividerConfig { fn calculate(config: &VoltageDividerConfig) -> Option { let calc = RCalc::new(vec![config.series; config.count]); + let mut context = HashMapContext::new(); + if let Some(v) = &config.extra_parameters { + for (i, p) in v.iter().enumerate() { + context + .set_value(format!("E{}", i + 1).into(), p.clone()) + .unwrap(); + } + } + + let context_rc = RefCell::new(context); calc.calc(|set| { if let Some(true) = config.resistance_min.map(|r| set.sum() < r) { return None; // Sum of resistance less than minimum @@ -74,16 +110,17 @@ fn calculate(config: &VoltageDividerConfig) -> Option { return None; // Sum of resistance larger than maximum } - // TODO: Storing this externally and using interior mutability - // could be helpful to avoid reallocating on every invocation. - let mut context = HashMapContext::new(); for i in 1..=config.count { - context + context_rc + .borrow_mut() .set_value(format!("R{}", i).into(), Value::Float(set.r(i))) .unwrap(); } - match config.expression.eval_with_context(&context) { + match config + .expression + .eval_with_context(context_rc.borrow().deref()) + { Ok(v) => Some((config.target - v.as_number().unwrap()).abs()), Err(e) => match &e { EvalexprError::DivisionError { divisor: d, .. } => { @@ -96,22 +133,24 @@ fn calculate(config: &VoltageDividerConfig) -> Option { return None; } } - panic_any(e) // No graceful way to handle this from the closure + panic_any(e.to_string()) // No graceful way to handle this from the closure } - _ => panic_any(e), + _ => panic_any(e.to_string()), }, } }) } /// `voltage_divider` computes values for resistor-based voltage dividers. -/// - Usage: vdiv(, , , (min resistance), (max resistance)) -/// - Example: vdiv(5.1, "(R1+R2)/R2*0.8", "E96", 500e3, 700e3) +/// - Usage: vdiv(, , , +/// {(, )}, ({extra 1}, {extra 2}, ...)) +/// - Example: vdiv(5.1, "(R1+R2)/R2*E1", "E96", (500e3, 700e3), (0.8)) /// - Output: (, , , ...) /// There can be arbitrary many resistors in the divider, but they must be named "R1", "R2", etc. /// The computed optimal resistance values are also presented in this order. The minimal and maximal -/// resistance limits are optional parameters, and only consider the sum of the resistances of all -/// resistors defined in the expression. +/// resistance pair is an optional parameter, and the limits only consider the sum of resistance of +/// all resistors defined in the expression. The "extra" parameters are optional external inputs for +/// the divider expression, and will be made available as "E1", "E2", etc. in order. pub(crate) fn voltage_divider(argument: &Value) -> EvalexprResult { let config = VoltageDividerConfig::parse(argument)?; if let Some(res) = calculate(&config) { diff --git a/tools/kicad/kicad_rs/src/bin/evaluator.rs b/tools/kicad/kicad_rs/src/bin/evaluator.rs index 77ffdb5..56c8a4b 100644 --- a/tools/kicad/kicad_rs/src/bin/evaluator.rs +++ b/tools/kicad/kicad_rs/src/bin/evaluator.rs @@ -1,6 +1,6 @@ use kicad_rs::error::DynamicResult; use kicad_rs::eval; -use kicad_rs::parser::{parse_schematic, SchematicFile}; +use kicad_rs::parser::SchematicTree; use std::env; // Main function, can return different kinds of errors @@ -8,9 +8,9 @@ fn main() -> DynamicResult<()> { let args: Vec = env::args().collect(); let path = std::path::Path::new(args.get(1).ok_or("expected file as first argument")?); - // Load the schematic file and parse it - let file = SchematicFile::load(path)?; - let mut schematic = parse_schematic(&file, String::new())?; + // Load the hierarchical schematic tree and parse it + let mut tree = SchematicTree::load(path)?; + let mut schematic = tree.parse()?; // Index the parsed schematic and use the index to evaluate it. The // index links to the schematic using mutable references, so that's @@ -18,8 +18,11 @@ fn main() -> DynamicResult<()> { let mut index = eval::index_schematic(&mut schematic)?; eval::evaluate_schematic(&mut index)?; - // TODO: Apply the internal schematic back to kicad_parse_gen::schematic::Schematic and print - println!("{:#?}", index); + // Update the fields of the components in the schematic tree based + // on the newly computed values and write the updated schematics + // back into the respective files + tree.update(&schematic)?; + tree.write()?; Ok(()) } diff --git a/tools/kicad/kicad_rs/src/parser.rs b/tools/kicad/kicad_rs/src/parser.rs index 9c0d482..2ed6db6 100644 --- a/tools/kicad/kicad_rs/src/parser.rs +++ b/tools/kicad/kicad_rs/src/parser.rs @@ -5,38 +5,92 @@ use std::path::Path; use crate::error::{errorf, DynamicResult}; use crate::types::*; -impl Schematic { - pub fn parse_id(path: &Path, id: String) -> DynamicResult { - let file = SchematicFile::load(path)?; - parse_schematic(&file, id) - } - - pub fn parse(path: &Path) -> DynamicResult { - Self::parse_id(path, String::new()) - } +// SchematicTree keeps track of all kicad_parse_gen +// Schematics in a hierarchical schematic configuration +#[derive(Debug)] +pub struct SchematicTree { + schematic: kicad_schematic::Schematic, + sub_schematics: HashMap, } -pub struct SchematicFile<'a> { - path: &'a Path, - content: kicad_schematic::Schematic, -} +impl SchematicTree { + // Load a hierarchical SchematicTree from the given base schematic path + pub fn load(path: &Path) -> DynamicResult { + let mut sub_schematics = HashMap::new(); + let schematic = kicad_schematic::parse_file(path)?; + for sub_sheet in schematic.sheets.iter() { + let filename = kicad_schematic::filename_for_sheet(&schematic, sub_sheet)?; + sub_schematics.insert(sub_sheet.name.clone(), SchematicTree::load(&filename)?); + } -impl<'a> SchematicFile<'a> { - pub fn load(path: &'a Path) -> DynamicResult { Ok(Self { - path, - // Read the schematic contents using kicad_parse_gen - content: kicad_parse_gen::read_schematic(path)?, + schematic, + sub_schematics, }) } + + // Parse the SchematicTree into our own nested Schematic struct + pub fn parse(&self) -> DynamicResult { + parse_schematic(self, String::new()) + } + + // Update the components in the kicad_parse_gen Schematic tree using the given + // nested Schematic struct (copy values from Attributes to ComponentFields) + pub fn update(&mut self, schematic: &Schematic) -> DynamicResult<()> { + // Update the fields of all components in this schematic + for component in schematic.components.iter() { + self.schematic + .modify_component(&component.labels.reference, |c| { + for attribute in component.attributes.iter() { + let name = attribute.name.as_str().or_default("Value"); + c.update_field(name, &attribute.value.to_string()); + c.update_field( + &format!("{}{}", name, "_expr"), + // TODO: Fix the escaping issue in upstream kicad_parse_gen + // and remove this field update altogether. + &escape(&attribute.expression), + ); + } + }) + } + + // Recursively update sub-schematics + for sub_schematic in schematic.sub_schematics.iter() { + match self.sub_schematics.get_mut(&sub_schematic.id) { + None => { + return Err(errorf(&format!( + "unknown sub-schematic: {}", + sub_schematic.id + ))) + } + Some(sub_tree) => sub_tree.update(sub_schematic)?, + }; + } + + Ok(()) + } + + // Write all Schematics in the SchematicTree hierarchy to their + // respective files, starting from the node this is called for + pub fn write(&self) -> DynamicResult<()> { + let path = self.schematic.filename.as_ref().ok_or(errorf(&format!( + "missing path for schematic {}", + self.schematic.description.title + )))?; + kicad_parse_gen::write_file(Path::new(path), &self.schematic.to_string())?; + for sub_schematic in self.sub_schematics.values() { + sub_schematic.write()?; + } + Ok(()) + } } /// Turns the given KiCad schematic into a recursive Schematic struct -pub fn parse_schematic(file: &SchematicFile, id: String) -> DynamicResult { +fn parse_schematic(file: &SchematicTree, id: String) -> DynamicResult { // Parse the fields for the schematic - let meta = parse_meta(&file)?; - let globals = parse_globals(&file)?; - let components = parse_components(&file)?; + let meta = parse_meta(&file.schematic)?; + let globals = parse_globals(&file.schematic)?; + let components = parse_components(&file.schematic)?; let sub_schematics = parse_sub_schematics(&file)?; // Construct and return the parsed schematic @@ -50,39 +104,40 @@ pub fn parse_schematic(file: &SchematicFile, id: String) -> DynamicResult DynamicResult { +fn parse_meta(kicad_sch: &kicad_schematic::Schematic) -> DynamicResult { // Only include non-empty comments let comments = vec![ - kicad_sch.content.description.comment1.as_str(), - kicad_sch.content.description.comment2.as_str(), - kicad_sch.content.description.comment3.as_str(), - kicad_sch.content.description.comment4.as_str(), + kicad_sch.description.comment1.as_str(), + kicad_sch.description.comment2.as_str(), + kicad_sch.description.comment3.as_str(), + kicad_sch.description.comment4.as_str(), ] .iter() .flat_map(|c| c.filter_empty()) .collect(); + let filename = if let Some(f) = &kicad_sch.filename { + Some(f.to_string_lossy().to_string()) + } else { + None + }; + Ok(SchematicMeta { - file_name: kicad_sch - .path - .file_name() - .map(|s| s.to_str()) - .flatten() - .or_empty_str(), - title: kicad_sch.content.description.title.as_str().filter_empty(), - date: kicad_sch.content.description.date.as_str().filter_empty(), - revision: kicad_sch.content.description.rev.as_str().filter_empty(), - company: kicad_sch.content.description.comp.as_str().filter_empty(), + filename, + title: kicad_sch.description.title.as_str().filter_empty(), + date: kicad_sch.description.date.as_str().filter_empty(), + revision: kicad_sch.description.rev.as_str().filter_empty(), + company: kicad_sch.description.comp.as_str().filter_empty(), comments, }) } /// Parses global definitions from text notes in the KiCad schematic -pub fn parse_globals(kicad_sch: &SchematicFile) -> DynamicResult> { +fn parse_globals(kicad_sch: &kicad_schematic::Schematic) -> DynamicResult> { let mut globals = Vec::new(); // Loop through the elements of the schematic, which includes text notes as well - for el in &kicad_sch.content.elements { + for el in &kicad_sch.elements { // Only match Text elements that have type Note let text_element = match el { kicad_schematic::Element::Text(t) => match t.t { @@ -134,11 +189,11 @@ pub fn parse_globals(kicad_sch: &SchematicFile) -> DynamicResult> } /// Parses the component definitions present in the given KiCad schematic -pub fn parse_components(kicad_sch: &SchematicFile) -> DynamicResult> { +fn parse_components(kicad_sch: &kicad_schematic::Schematic) -> DynamicResult> { let mut components = Vec::new(); // Walk through all components in the sheet - for comp in kicad_sch.content.components() { + for comp in kicad_sch.components() { // Require comp.name to be non-empty if comp.name.is_empty() { return Err(errorf("Every component must have a name")); @@ -211,8 +266,10 @@ pub fn parse_components(kicad_sch: &SchematicFile) -> DynamicResult DynamicResult DynamicResult> { +fn parse_sub_schematics(tree: &SchematicTree) -> DynamicResult> { let mut sub_schematics = Vec::new(); // Recursively traverse and parse the sub-schematics - for sub_sheet in &kicad_sch.content.sheets { - let p = kicad_sch - .path - .parent() - .unwrap_or(Path::new("")) - .join(Path::new(&sub_sheet.filename)); - sub_schematics.push(Schematic::parse_id(&p, sub_sheet.name.clone())?); + for (id, schematic) in tree.sub_schematics.iter() { + sub_schematics.push(parse_schematic(schematic, id.clone())?); } Ok(sub_schematics) @@ -363,6 +415,31 @@ impl> SplitCharN for Option { } } +// OrDefault provides a way to substitute the default value of a variable. For +// example, the default value of a &str type is "". If the caller equals this +// default value, the returned value is replaced with the new passed-in default +// of the same type, otherwise the current value of the caller is returned. +trait OrDefault<'a, T> { + fn or_default(self, default: T) -> T; +} + +// OrDefault is implemented for all types that implement Default and PartialEq +// (for comparing against their type-specific default). The built-in trait Default +// allows fetching the default value to compare against, e.g. "" for string-like +// types and 0 for number-like types as the respective type T. +impl<'a, T: Default + PartialEq> OrDefault<'a, T> for T { + fn or_default(self, default: T) -> T { + if self == Default::default() { + // If the caller matches its own default, return the new given default instead + return default; + } + + self // Otherwise return the current value of the caller + } +} + +// This is a workaround for broken string escaping in kicad_parse_gen. +// TODO: Fix escaping issue upstream and remove this. fn unescape(s: &str) -> String { let mut res = String::new(); let mut prev = 0 as char; @@ -387,6 +464,21 @@ fn unescape(s: &str) -> String { res } +// This is a workaround for broken string escaping in kicad_parse_gen. +// TODO: Fix escaping issue upstream and remove this. +fn escape(s: &str) -> String { + let mut res = String::new(); + + for c in s.chars() { + match c { + '"' => res.push_str(r#"\""#), // Escape backslashes + c => res.push(c), + } + } + + res +} + #[cfg(test)] mod tests { use super::*; @@ -399,4 +491,13 @@ mod tests { unescape(r"vdiv(5.1, \(R1+\\R2\\\\)/R2*0.8\, 'E96', 500e3, 700e3)") ) } + + #[test] + fn test_escape() { + assert_eq!(r#"\\""#, escape(r#"\""#)); + assert_eq!( + r#"vdiv(5.1, \"E1*(R1+R2)/R2\", \"E96\", (500e3, 700e3), (0.8))"#, + escape(r#"vdiv(5.1, "E1*(R1+R2)/R2", "E96", (500e3, 700e3), (0.8))"#) + ) + } } diff --git a/tools/kicad/kicad_rs/src/types.rs b/tools/kicad/kicad_rs/src/types.rs index 3e08fae..a93eda9 100644 --- a/tools/kicad/kicad_rs/src/types.rs +++ b/tools/kicad/kicad_rs/src/types.rs @@ -28,7 +28,9 @@ pub struct Schematic { #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub struct SchematicMeta { - pub file_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub filename: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub title: Option,