diff --git a/tools/kicad/Cargo.toml b/tools/kicad/Cargo.toml new file mode 100644 index 0000000..a6ea54d --- /dev/null +++ b/tools/kicad/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["kicad_rs", "kicad_functions"] diff --git a/tools/kicad/kicad_functions/Cargo.toml b/tools/kicad/kicad_functions/Cargo.toml new file mode 100644 index 0000000..3873845 --- /dev/null +++ b/tools/kicad/kicad_functions/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "kicad_functions" +version = "0.1.0" +authors = ["Dennis Marttinen "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +evalexpr = "6.1" +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/idx.rs b/tools/kicad/kicad_functions/src/idx.rs new file mode 100644 index 0000000..abe5ef6 --- /dev/null +++ b/tools/kicad/kicad_functions/src/idx.rs @@ -0,0 +1,23 @@ +use crate::util::err; +use evalexpr::{EvalexprError, EvalexprResult, Value}; + +/// `index` retrieves tuple values based on the given index. +/// - Usage: idx(, ) +/// - Example: idx(("a", "b", "c"), 1) -> "b" +/// - Output: i:th value in the tuple (zero-indexed) +pub(crate) fn index(argument: &Value) -> EvalexprResult { + let args = argument.as_tuple()?; + if let [target, index] = &args[..] { + let index = index.as_int()?; + return target + .as_tuple()? + .get(index as usize) + .ok_or(EvalexprError::CustomMessage(format!( + "index out of bounds: {}", + index + ))) + .map(|v| v.clone()); + } + + err(&format!("unsupported argument count: {}", args.len())) +} diff --git a/tools/kicad/kicad_functions/src/lib.rs b/tools/kicad/kicad_functions/src/lib.rs new file mode 100644 index 0000000..1f9e37f --- /dev/null +++ b/tools/kicad/kicad_functions/src/lib.rs @@ -0,0 +1,14 @@ +mod idx; +pub mod util; +mod vdiv; + +use evalexpr::{EvalexprError, EvalexprResult, Value}; + +// Match function for all custom functions available in the kicad_rs evaluator +pub fn call_function(identifier: &str, argument: &Value) -> EvalexprResult { + match identifier { + "idx" => idx::index(argument), + "vdiv" => vdiv::voltage_divider(argument), + other => Err(EvalexprError::FunctionIdentifierNotFound(other.into())), + } +} diff --git a/tools/kicad/kicad_functions/src/util.rs b/tools/kicad/kicad_functions/src/util.rs new file mode 100644 index 0000000..ac436e0 --- /dev/null +++ b/tools/kicad/kicad_functions/src/util.rs @@ -0,0 +1,6 @@ +use evalexpr::{EvalexprError, EvalexprResult}; + +/// Returns an `EvalexprResult` with a `EvalexprError::CustomMessage` error +pub fn err(msg: &str) -> EvalexprResult { + Err(EvalexprError::CustomMessage(msg.into())) +} diff --git a/tools/kicad/kicad_functions/src/vdiv.rs b/tools/kicad/kicad_functions/src/vdiv.rs new file mode 100644 index 0000000..8c59931 --- /dev/null +++ b/tools/kicad/kicad_functions/src/vdiv.rs @@ -0,0 +1,137 @@ +use crate::util::err; +use evalexpr::{ + ContextWithMutableVariables, EvalexprError, EvalexprResult, HashMapContext, Node, Value, +}; +use resistor_calc::{RCalc, RRes, RSeries}; +use std::collections::HashSet; +use std::panic::panic_any; + +fn parse_series(str: &str) -> EvalexprResult<&'static RSeries> { + match str.trim() { + "E3" => Ok(&resistor_calc::E3), + "E6" => Ok(&resistor_calc::E6), + "E12" => Ok(&resistor_calc::E12), + "E24" => Ok(&resistor_calc::E24), + "E48" => Ok(&resistor_calc::E48), + "E96" => Ok(&resistor_calc::E96), + _ => err(&format!("unknown resistor series: {}", str)), + } +} + +// evalexpr is not smart enough to distinguish identifiers on its own +fn unique_identifiers(e: &Node) -> usize { + let mut set = HashSet::new(); + e.iter_variable_identifiers().for_each(|i| { + set.insert(i); + }); + set.len() +} + +struct VoltageDividerConfig { + target: f64, + expression: Node, + count: usize, + series: &'static RSeries, + resistance_min: Option, + resistance_max: 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()?; + + // 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); + + Ok(Self { + target: target.as_number()?, + expression, + count, + series: parse_series(&series.as_string()?)?, + resistance_min, + resistance_max, + }) + } else { + err(&format!("unsupported argument count: {}", tuple.len())) + } + } +} + +fn calculate(config: &VoltageDividerConfig) -> Option { + let calc = RCalc::new(vec![config.series; config.count]); + + calc.calc(|set| { + if let Some(true) = config.resistance_min.map(|r| set.sum() < r) { + return None; // Sum of resistance less than minimum + } + + if let Some(true) = config.resistance_max.map(|r| set.sum() > r) { + 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 + .set_value(format!("R{}", i).into(), Value::Float(set.r(i))) + .unwrap(); + } + + match config.expression.eval_with_context(&context) { + Ok(v) => Some((config.target - v.as_number().unwrap()).abs()), + Err(e) => match &e { + EvalexprError::DivisionError { divisor: d, .. } => { + if let Ok(n) = d.as_number() { + if n == 0.0 { + // This soft-catch may be a bit redundant. Based on some testing the + // internal conversions in evalexpr cause zero values to deviate + // slightly from zero, thus avoiding division by zero even if you + // explicitly write a zero division into the voltage divider equation. + return None; + } + } + panic_any(e) // No graceful way to handle this from the closure + } + _ => panic_any(e), + }, + } + }) +} + +/// `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) +/// - 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. +pub(crate) fn voltage_divider(argument: &Value) -> EvalexprResult { + let config = VoltageDividerConfig::parse(argument)?; + if let Some(res) = calculate(&config) { + // Take the first result, these are ordered by increasing error + if let Some((v, set)) = res.iter().next() { + let voltage = config.target + fixed_to_floating(*v); + let mut tuple = vec![Value::from(voltage)]; + for i in 1..=config.count { + tuple.push(Value::from(set.r(i))); + } + return Ok(Value::from(tuple)); + } + } + + err(&format!("no solution found: {}", argument)) +} + +// resistor_calc outputs error quantities as "fixed point" numbers by multiplying +// a float by 1e9, rounding the result and converting to u64. We need to undo +// that procedure here. +fn fixed_to_floating(value: u64) -> f64 { + (value as f64) / 1e9 +} diff --git a/tools/kicad_rs/Cargo.toml b/tools/kicad/kicad_rs/Cargo.toml similarity index 90% rename from tools/kicad_rs/Cargo.toml rename to tools/kicad/kicad_rs/Cargo.toml index c716e68..bec3c9a 100644 --- a/tools/kicad_rs/Cargo.toml +++ b/tools/kicad/kicad_rs/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] evalexpr = "6.1" +kicad_functions = { path = "../kicad_functions" } kicad_parse_gen = { git = "https://github.com/productize/kicad-parse-gen", branch = "kicad5" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/tools/kicad_rs/src/bin/classifier.rs b/tools/kicad/kicad_rs/src/bin/classifier.rs similarity index 100% rename from tools/kicad_rs/src/bin/classifier.rs rename to tools/kicad/kicad_rs/src/bin/classifier.rs diff --git a/tools/kicad_rs/src/bin/evaluator.rs b/tools/kicad/kicad_rs/src/bin/evaluator.rs similarity index 100% rename from tools/kicad_rs/src/bin/evaluator.rs rename to tools/kicad/kicad_rs/src/bin/evaluator.rs diff --git a/tools/kicad_rs/src/bin/parser.rs b/tools/kicad/kicad_rs/src/bin/parser.rs similarity index 100% rename from tools/kicad_rs/src/bin/parser.rs rename to tools/kicad/kicad_rs/src/bin/parser.rs diff --git a/tools/kicad_rs/src/classifier.rs b/tools/kicad/kicad_rs/src/classifier.rs similarity index 100% rename from tools/kicad_rs/src/classifier.rs rename to tools/kicad/kicad_rs/src/classifier.rs diff --git a/tools/kicad_rs/src/codec.rs b/tools/kicad/kicad_rs/src/codec.rs similarity index 100% rename from tools/kicad_rs/src/codec.rs rename to tools/kicad/kicad_rs/src/codec.rs diff --git a/tools/kicad_rs/src/error.rs b/tools/kicad/kicad_rs/src/error.rs similarity index 100% rename from tools/kicad_rs/src/error.rs rename to tools/kicad/kicad_rs/src/error.rs diff --git a/tools/kicad_rs/src/eval.rs b/tools/kicad/kicad_rs/src/eval.rs similarity index 100% rename from tools/kicad_rs/src/eval.rs rename to tools/kicad/kicad_rs/src/eval.rs diff --git a/tools/kicad_rs/src/eval/entry.rs b/tools/kicad/kicad_rs/src/eval/entry.rs similarity index 100% rename from tools/kicad_rs/src/eval/entry.rs rename to tools/kicad/kicad_rs/src/eval/entry.rs diff --git a/tools/kicad_rs/src/eval/index.rs b/tools/kicad/kicad_rs/src/eval/index.rs similarity index 92% rename from tools/kicad_rs/src/eval/index.rs rename to tools/kicad/kicad_rs/src/eval/index.rs index aebd23c..8a7b45e 100644 --- a/tools/kicad_rs/src/eval/index.rs +++ b/tools/kicad/kicad_rs/src/eval/index.rs @@ -74,9 +74,8 @@ impl<'a> Context for SheetIndex<'a> { .flatten() } - fn call_function(&self, _identifier: &str, _argument: &Value) -> EvalexprResult { - // TODO: Fixed function set (voltage divider etc.) - unimplemented!("functions are currently unsupported"); + fn call_function(&self, identifier: &str, argument: &Value) -> EvalexprResult { + kicad_functions::call_function(identifier, argument) } } diff --git a/tools/kicad_rs/src/eval/path.rs b/tools/kicad/kicad_rs/src/eval/path.rs similarity index 100% rename from tools/kicad_rs/src/eval/path.rs rename to tools/kicad/kicad_rs/src/eval/path.rs diff --git a/tools/kicad_rs/src/labels.rs b/tools/kicad/kicad_rs/src/labels.rs similarity index 100% rename from tools/kicad_rs/src/labels.rs rename to tools/kicad/kicad_rs/src/labels.rs diff --git a/tools/kicad_rs/src/lib.rs b/tools/kicad/kicad_rs/src/lib.rs similarity index 100% rename from tools/kicad_rs/src/lib.rs rename to tools/kicad/kicad_rs/src/lib.rs diff --git a/tools/kicad_rs/src/parser.rs b/tools/kicad/kicad_rs/src/parser.rs similarity index 91% rename from tools/kicad_rs/src/parser.rs rename to tools/kicad/kicad_rs/src/parser.rs index ca9f6d2..9c0d482 100644 --- a/tools/kicad_rs/src/parser.rs +++ b/tools/kicad/kicad_rs/src/parser.rs @@ -210,8 +210,10 @@ pub fn parse_components(kicad_sch: &SchematicFile) -> DynamicResult> SplitCharN for Option { } } } + +fn unescape(s: &str) -> String { + let mut res = String::new(); + let mut prev = 0 as char; + + let mut iter = s.chars().peekable(); + while let Some(c) = iter.next() { + let mut next_prev = c; + match c { + '\\' => { + if prev == '\\' { + res.push('\\'); + next_prev = 0 as char; // Remove duplicate backslashes + } else if iter.peek().map(|next| next != &'\\').unwrap_or(true) { + res.push('"'); // A lone backslash is actually a missing double quote + } + } + c => res.push(c), + } + prev = next_prev; + } + + res +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unescape() { + assert_eq!(r#"\""#, unescape(r"\\\")); + assert_eq!( + r#"vdiv(5.1, "(R1+\R2\\)/R2*0.8", 'E96', 500e3, 700e3)"#, + unescape(r"vdiv(5.1, \(R1+\\R2\\\\)/R2*0.8\, 'E96', 500e3, 700e3)") + ) + } +} diff --git a/tools/kicad_rs/src/requirements.rs b/tools/kicad/kicad_rs/src/requirements.rs similarity index 100% rename from tools/kicad_rs/src/requirements.rs rename to tools/kicad/kicad_rs/src/requirements.rs diff --git a/tools/kicad_rs/src/types.rs b/tools/kicad/kicad_rs/src/types.rs similarity index 100% rename from tools/kicad_rs/src/types.rs rename to tools/kicad/kicad_rs/src/types.rs