app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Add the Rust source, flow definition, assets and install scripts
cf9809b
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
//! Just enough of a `.kicad_pcb` to compare the copied board with the photos: each footprint's
//! reference, side, position and rotation, and the copper layer count. A small S-expression scanner;
//! nothing here writes a board.
#[derive(Debug, Clone)]
pub struct Footprint { pub reference: String, pub layer: String, pub x: f64, pub y: f64, pub rot: f64 }
/// The child lists of the list that opens at `open` (byte index of its '('), as (start, end) spans.
fn children(t: &[u8], open: usize) -> Vec<(usize, usize)> {
let mut out = vec![];
let (mut i, mut depth, mut in_str, mut start) = (open + 1, 1usize, false, 0usize);
while i < t.len() && depth > 0 {
let c = t[i];
if in_str {
if c == b'\\' { i += 1; } else if c == b'"' { in_str = false; }
} else if c == b'"' { in_str = true; }
else if c == b'(' { if depth == 1 { start = i; } depth += 1; }
else if c == b')' { depth -= 1; if depth == 1 { out.push((start, i + 1)); } }
i += 1;
}
out
}
fn head(s: &str) -> &str { s[1..].split(|c: char| c.is_whitespace() || c == ')').next().unwrap_or("") }
fn quoted(s: &str, n: usize) -> Option<String> {
let parts: Vec<&str> = s.split('"').collect();
parts.get(2 * n + 1).map(|x| x.to_string())
}
fn nums(s: &str) -> Vec<f64> {
s.trim_start_matches('(').trim_end_matches(')').split_whitespace().skip(1).filter_map(|x| x.parse().ok()).collect()
}
pub fn footprints(text: &str) -> Vec<Footprint> {
let t = text.as_bytes();
let Some(root) = text.find('(') else { return vec![] };
let mut out = vec![];
for (s, e) in children(t, root) {
let fp = &text[s..e];
if head(fp) != "footprint" && head(fp) != "module" { continue; }
let (mut reference, mut layer, mut at) = (String::new(), String::new(), vec![]);
for (cs, ce) in children(t, s) {
let c = &text[cs..ce];
match head(c) {
"layer" if layer.is_empty() => layer = quoted(c, 0).unwrap_or_default(),
"at" if at.is_empty() => at = nums(c),
"property" if reference.is_empty() && quoted(c, 0).as_deref() == Some("Reference") => reference = quoted(c, 1).unwrap_or_default(),
"fp_text" if reference.is_empty() && c.starts_with("(fp_text reference") => reference = quoted(c, 0).unwrap_or_default(),
_ => {}
}
}
out.push(Footprint { reference, layer, x: *at.first().unwrap_or(&0.0), y: *at.get(1).unwrap_or(&0.0), rot: *at.get(2).unwrap_or(&0.0) });
}
out
}
/// Copper layers declared in the board's `(layers ...)` section.
pub fn copper_layers(text: &str) -> usize {
let t = text.as_bytes();
let Some(root) = text.find('(') else { return 0 };
for (s, _) in children(t, root) {
if head(&text[s..]) != "layers" { continue; }
return children(t, s).iter().filter(|(a, b)| quoted(&text[*a..*b], 0).map(|n| n.ends_with(".Cu")).unwrap_or(false)).count();
}
0
}
#[cfg(test)]
mod tests {
use super::*;
const PCB: &str = r#"(kicad_pcb (version 20250114) (generator "pcbnew")
(layers (0 "F.Cu" signal) (4 "In1.Cu" power) (6 "In2.Cu" power) (2 "B.Cu" signal) (9 "F.Adhes" user "F.Adhesive"))
(footprint "Resistor_SMD:R_0603" (layer "F.Cu") (uuid "a")
(at 10.5 20.25 90)
(property "Reference" "R1" (at 0 -1.4 90) (layer "F.SilkS"))
(pad "1" smd rect (at -0.8 0) (size 0.9 0.9) (layers "F.Cu"))
)
(footprint "Package_DFN_QFN:QFN-48" (layer "B.Cu") (uuid "b")
(at 30 40 225)
(property "Reference" "U1 (\"x\")" (at 0 0) (layer "B.SilkS"))
)
(gr_text "R9" (at 1 1))
)"#;
#[test]
fn reads_footprints_and_layers() {
let f = footprints(PCB);
assert_eq!(f.len(), 2);
assert_eq!(f[0].reference, "R1");
assert_eq!((f[0].x, f[0].y, f[0].rot), (10.5, 20.25, 90.0));
assert_eq!(f[1].layer, "B.Cu");
assert_eq!(f[1].rot, 225.0);
assert_eq!(copper_layers(PCB), 4);
}
}