app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
John Lauer
Publish 0.2.3
6e27c4b
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
//! Net names the way a spec and a board can agree on them. A board built from a hierarchical
//! schematic names root-sheet nets `/GND`, `/VIN`; a spec (or a flat board) says `GND`, `VIN`.
//! `netlist check` already treats the two as one net; the same rule resolves a spec's net names
//! against the board: an exact name always wins, and a name that differs only by the leading `/`
//! matches only when exactly one board net does.
use serde_json::Value;
use std::collections::BTreeSet;
/// A net name without the root sheet's leading `/` (`/VIN` -> `VIN`). A sub-sheet path
/// (`/power/VIN`) keeps its hierarchy.
pub fn norm_net(n: &str) -> String {
let t = n.trim();
t.strip_prefix('/').unwrap_or(t).to_string()
}
/// The board net a spec name means: the exact name when the board has it, else the one board net
/// equal to it under `norm_net`. None when there is none, or more than one (ambiguous).
pub fn resolve<'a, I>(name: &str, board_nets: I) -> Option<String>
where
I: IntoIterator<Item = &'a String>,
{
let want = norm_net(name);
let mut hits: Vec<&String> = Vec::new();
for n in board_nets {
if n == name {
return Some(n.clone());
}
if norm_net(n) == want {
hits.push(n);
}
}
(hits.len() == 1).then(|| hits[0].clone())
}
/// Board nets close to a name the board does not have, for a refusal: the same name up to the leading
/// `/`, else the same up to case.
pub fn close<'a, I>(name: &str, board_nets: I) -> Vec<String>
where
I: IntoIterator<Item = &'a String>,
{
let all: Vec<&String> = board_nets.into_iter().collect();
let want = norm_net(name);
let same: Vec<String> = all.iter().filter(|n| n.as_str() != name && norm_net(n) == want).map(|n| n.to_string()).collect();
if !same.is_empty() {
return same;
}
all.iter().filter(|n| n.as_str() != name && norm_net(n).eq_ignore_ascii_case(&want)).map(|n| n.to_string()).collect()
}
/// Every net name a `.kicad_pcb` declares or uses (`(net 3 "/GND")`, KiCad 10's `(net "/GND")`),
/// read without parsing the whole board.
pub fn names_in_text(text: &str) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let b = text.as_bytes();
let mut i = 0;
while let Some(off) = text[i..].find("(net") {
let mut j = i + off + 4;
i = j;
// `(net` followed by whitespace only (not `(net_name`, `(net_class`)
if j >= b.len() || !b[j].is_ascii_whitespace() { continue; }
while j < b.len() && b[j].is_ascii_whitespace() { j += 1; }
while j < b.len() && b[j].is_ascii_digit() { j += 1; }
while j < b.len() && b[j].is_ascii_whitespace() { j += 1; }
if j >= b.len() || b[j] != b'"' { continue; }
j += 1;
let mut name = String::new();
let mut chars = text[j..].char_indices();
let mut end = None;
while let Some((k, c)) = chars.next() {
match c {
'\\' => { if let Some((_, e)) = chars.next() { name.push(e); } }
'"' => { end = Some(j + k + 1); break; }
_ => name.push(c),
}
}
if let Some(e) = end {
if !name.is_empty() { out.insert(name); }
i = e;
}
}
out
}
/// Rewrite the net names a spec gives (pours[].net, planes, wideNets, midNets, netClearance and loads
/// keys, switchNodes, hot.*.tabNet, thermalVias[].net, stitchVias[].net, planeUnder[].net) to the
/// board's spelling when `resolve` finds exactly one. Returns `(spec name, board name)` per rename.
pub fn align_spec(spec: &mut Value, board_nets: &BTreeSet<String>) -> Vec<(String, String)> {
let mut renames: Vec<(String, String)> = Vec::new();
let mut fix = |s: &str| -> Option<String> {
let r = resolve(s, board_nets.iter())?;
if r == s { return None; }
if !renames.iter().any(|(a, _)| a == s) { renames.push((s.to_string(), r.clone())); }
Some(r)
};
let Some(obj) = spec.as_object_mut() else { return renames };
// a field `key` holding a net name, in each object of an array (or one object)
let in_items = |v: Option<&mut Value>, key: &str, fix: &mut dyn FnMut(&str) -> Option<String>| {
let Some(v) = v else { return };
let items: Vec<&mut Value> = match v { Value::Array(a) => a.iter_mut().collect(), Value::Object(_) => vec![v], _ => vec![] };
for it in items {
if let Some(n) = it.get(key).and_then(|n| n.as_str()).and_then(|n| fix(n)) { it[key] = Value::String(n); }
}
};
in_items(obj.get_mut("pours"), "net", &mut fix);
in_items(obj.get_mut("thermalVias"), "net", &mut fix);
in_items(obj.get_mut("stitchVias"), "net", &mut fix);
in_items(obj.get_mut("planeUnder"), "net", &mut fix);
for key in ["planes", "wideNets", "midNets", "netClearance", "loads"] {
let Some(m) = obj.get_mut(key).and_then(|v| v.as_object_mut()) else { continue };
let keys: Vec<String> = m.keys().cloned().collect();
for k in keys {
if let Some(n) = fix(&k) {
// the board's own spelling already in the map wins over the alias
if m.contains_key(&n) { continue; }
let v = m.remove(&k).unwrap();
m.insert(n, v);
}
}
}
if let Some(a) = obj.get_mut("switchNodes").and_then(|v| v.as_array_mut()) {
for x in a.iter_mut() {
if let Some(n) = x.as_str().and_then(|s| fix(s)) { *x = Value::String(n); }
}
}
if let Some(hot) = obj.get_mut("hot").and_then(|v| v.as_object_mut()) {
for (_, h) in hot.iter_mut() {
if let Some(n) = h.get("tabNet").and_then(|n| n.as_str()).and_then(|n| fix(n)) { h["tabNet"] = Value::String(n); }
}
}
renames
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn set(a: &[&str]) -> BTreeSet<String> { a.iter().map(|s| s.to_string()).collect() }
#[test]
fn hierarchical_name_matches_when_unambiguous() {
let b = set(&["/GND", "/VIN", "Net-(U1-SW)"]);
assert_eq!(resolve("GND", b.iter()).as_deref(), Some("/GND"));
assert_eq!(resolve("/VIN", b.iter()).as_deref(), Some("/VIN"));
assert_eq!(resolve("VOUT", b.iter()), None);
// the other way round: spec `/GND`, flat board `GND`
assert_eq!(resolve("/GND", set(&["GND"]).iter()).as_deref(), Some("GND"));
// a sub-sheet path keeps its hierarchy
assert_eq!(resolve("VIN", set(&["/power/VIN"]).iter()), None);
}
#[test]
fn exact_match_wins_over_the_alias() {
let b = set(&["GND", "/GND"]);
assert_eq!(resolve("GND", b.iter()).as_deref(), Some("GND"));
assert_eq!(resolve("/GND", b.iter()).as_deref(), Some("/GND"));
}
#[test]
fn close_names_the_board_spelling() {
let b = set(&["/GND", "/VIN"]);
assert_eq!(close("GND", b.iter()), vec!["/GND".to_string()]);
assert_eq!(close("gnd", b.iter()), vec!["/GND".to_string()]);
assert!(close("VOUT", b.iter()).is_empty());
}
#[test]
fn names_from_board_text() {
let t = r#"(kicad_pcb (net 0 "") (net 1 "/GND") (net 2 "/VIN") (footprint "x" (pad "1" smd rect (net "/SW")) (zone (net 1) (net_name "/GND"))))"#;
assert_eq!(names_in_text(t), set(&["/GND", "/VIN", "/SW"]));
}
#[test]
fn spec_names_follow_the_board() {
let mut spec = json!({
"pours": [{"net": "GND", "outline": true}, {"net": "VOUT"}],
"planes": {"GND": "In1.Cu"}, "wideNets": {"VIN": [1.0]}, "midNets": {"/VIN": [0.5]},
"hot": {"U1": {"watts": 0.2, "tabNet": "GND"}}, "switchNodes": ["SW"],
"thermalVias": [{"pad": "U1.7", "net": "GND"}], "planeUnder": {"net": "GND", "layer": "B.Cu"}
});
let r = align_spec(&mut spec, &set(&["/GND", "/VIN", "/SW"]));
assert_eq!(spec["pours"][0]["net"], "/GND");
assert_eq!(spec["pours"][1]["net"], "VOUT");
assert_eq!(spec["planes"], json!({"/GND": "In1.Cu"}));
assert_eq!(spec["wideNets"], json!({"/VIN": [1.0]}));
assert_eq!(spec["midNets"], json!({"/VIN": [0.5]}));
assert_eq!(spec["hot"]["U1"]["tabNet"], "/GND");
assert_eq!(spec["switchNodes"], json!(["/SW"]));
assert_eq!(spec["thermalVias"][0]["net"], "/GND");
assert_eq!(spec["planeUnder"]["net"], "/GND");
assert!(r.contains(&("GND".into(), "/GND".into())));
// both spellings on the board: nothing moves
let mut s2 = json!({"pours": [{"net": "GND"}], "hot": {"U1": {"tabNet": "GND"}}});
assert!(align_spec(&mut s2, &set(&["GND", "/GND"])).is_empty());
assert_eq!(s2["pours"][0]["net"], "GND");
}
}