Skip to content
This repository was archived by the owner on Nov 11, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/kicad/kicad_functions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
2 changes: 1 addition & 1 deletion tools/kicad/kicad_functions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod vdiv;

use evalexpr::{EvalexprError, EvalexprResult, Value};

// Match function for all custom functions available in the kicad_rs evaluator
/// Matching function for all custom functions provided by the kicad_rs evaluator
pub fn call_function(identifier: &str, argument: &Value) -> EvalexprResult<Value> {
match identifier {
"idx" => idx::index(argument),
Expand Down
81 changes: 60 additions & 21 deletions tools/kicad/kicad_functions/src/vdiv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -18,35 +21,57 @@ 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()
Comment on lines +25 to 32
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the HashSet used here? Couldn't this be just the following?

fn resistor_identifiers(e: &Node) -> usize {
    let re = Regex::new(r"^R[1-9][0-9]*$").unwrap();
    e.iter_variable_identifiers()
        .filter(|i| re.is_match(i)) // Match only R? identifiers
        .count()
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say you have parsed an expression of the form R2 = R1 * R1. What iter_variable_identifiers() is going to output is simply all variable identifiers of the parsed node tree in order, i.e. the list R2, R1, R1. The HashSet is used to keep track of already encountered identifiers for a relatively simple way to avoid counting a given identifier more than once.

}

// Helper for parsing of potentially single-element tuples
fn parse_tuple(v: &Value) -> Vec<Value> {
if let Ok(t) = v.as_tuple() {
return t;
}

vec![v.clone()]
}

struct VoltageDividerConfig {
target: f64,
expression: Node,
count: usize,
series: &'static RSeries,
resistance_min: Option<f64>,
resistance_max: Option<f64>,
extra_parameters: Option<Vec<Value>>,
}

impl VoltageDividerConfig {
fn parse(v: &Value) -> EvalexprResult<Self> {
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()?,
Expand All @@ -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()))
Expand All @@ -65,6 +91,16 @@ impl VoltageDividerConfig {
fn calculate(config: &VoltageDividerConfig) -> Option<RRes> {
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
Expand All @@ -74,16 +110,17 @@ fn calculate(config: &VoltageDividerConfig) -> Option<RRes> {
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, .. } => {
Expand All @@ -96,22 +133,24 @@ fn calculate(config: &VoltageDividerConfig) -> Option<RRes> {
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(<target voltage>, <divider expression>, <resistor series>, (min resistance), (max resistance))
/// - Example: vdiv(5.1, "(R1+R2)/R2*0.8", "E96", 500e3, 700e3)
/// - Output: (<closest voltage>, <R1 value>, <R2 value>, ...)
/// - Usage: vdiv(\<target voltage\>, \<divider expression\>, \<resistor series\>,
/// {(\<min resistance\>, \<max resistance\>)}, ({extra 1}, {extra 2}, ...))
/// - Example: vdiv(5.1, "(R1+R2)/R2*E1", "E96", (500e3, 700e3), (0.8))
/// - Output: (\<closest voltage\>, \<R1 value\>, \<R2 value\>, ...)
/// 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<Value> {
let config = VoltageDividerConfig::parse(argument)?;
if let Some(res) = calculate(&config) {
Expand Down