--- a/crates/adom-aiflow/src/programming.rs +++ b/crates/adom-aiflow/src/programming.rs @@ -69,6 +69,48 @@ if tp { Err(format!("{reference} is a test pad with no silkscreen label")) } else { Err(format!("{reference} ({}) is neither a machine contact nor a labelled test pad", fp.lib.rsplit(':').next().unwrap_or(&fp.lib))) } } +/// Cable access is an explicit alternative to a workcell fixture, not a declaration +/// that arbitrary IC pads are reachable. Check declared connector pins against saved copper. +fn cable_usb_check(b: &Board, rec: &Value) -> Result<(), Vec> { + let mut problems = Vec::new(); + let connector = rec["connector"].as_str().unwrap_or(""); + let is_usb = b.footprints.get(connector).is_some_and(|f| { + let lib = f.lib.to_ascii_lowercase(); + connector.starts_with('J') && lib.contains("usb") + }); + if !is_usb { problems.push("connector must name an existing USB connector footprint".into()); } + let target_ref = rec["targetRef"].as_str().unwrap_or(""); + let target = rec["target"].as_str().unwrap_or(""); + if !targets(b).iter().any(|(r, m)| r == target_ref && m == target && m.starts_with("ESP32")) { + problems.push("targetRef/target must match the programmable ESP32 part on the board".into()); + } + if rec["boot"].as_str().map_or(true, |x| x.trim().is_empty()) { + problems.push("boot recovery instructions missing".into()); + } + let mut nets = Vec::new(); + for signal in ["D+", "D-", "GND", "VBUS"] { + let pin = &rec["connectorPins"][signal]; + let net = pin["net"].as_str().unwrap_or(""); + let names = pin["pads"].as_array(); + if net.is_empty() || names.map_or(true, |a| a.is_empty()) { + problems.push(format!("connectorPins.{signal} needs nonempty pads and net")); + continue; + } + for name in names.unwrap() { + let pad = name.as_str().unwrap_or(""); + if !b.pads.iter().any(|p| p.reference == connector && p.name == pad && p.net == net) { + problems.push(format!("{signal}: {connector}.{pad} does not carry declared net {net}")); + } + } + nets.push(net); + } + nets.sort(); nets.dedup(); + if nets.len() != 4 { problems.push("USB data, ground and VBUS must be four distinct nets".into()); } + // Pin membership is evidence of the connector mapping only. Full connectivity, + // MCU routing and programming behavior remain the native gate and bench review. + if problems.is_empty() { Ok(()) } else { Err(problems) } +} + /// The finish gate: Ok when the board has no programmable part or its record is complete and every pin /// is reachable; Err(problems) otherwise. pub fn check(board_path: &str, b: &Board, spec: &Value) -> Result<(), Vec> { @@ -76,6 +118,7 @@ if t.is_empty() { return Ok(()); } let names = t.iter().map(|(r, m)| format!("{r} {m}")).collect::>().join(", "); let Some(rec) = record(board_path, spec) else { return Err(vec![format!("no `programming` entry in board-release.json for {names}")]) }; + if rec["interface"] == "usb-serial-jtag" && rec["access"] == "usb-cable" { return cable_usb_check(b, &rec); } let mut p = Vec::new(); let iface = rec["interface"].as_str().unwrap_or(""); let pins = rec["pins"].as_object().cloned().unwrap_or_default(); @@ -99,7 +142,7 @@ if t.is_empty() { return "programming: no programmable part: programming not required".into(); } let names = t.iter().map(|(r, m)| format!("{r} {m}")).collect::>().join(", "); match result { - Ok(()) => format!("programming: {names}, interface {} on reachable pins", record(board_path, spec).and_then(|r| r["interface"].as_str().map(str::to_string)).unwrap_or_default()), + Ok(()) => format!("programming: {names}, interface {} with declared access checked", record(board_path, spec).and_then(|r| r["interface"].as_str().map(str::to_string)).unwrap_or_default()), Err(p) => format!("programming: {names}, {} problem(s) (the gate below)", p.len()), } } @@ -177,7 +220,35 @@ assert_eq!(summary("/nonexistent/b.kicad_pcb", &b, &json!({}), &Ok(())), "programming: no programmable part: programming not required"); let b = Board::from_text("/nonexistent/b.kicad_pcb", BOARD.to_string()).unwrap(); let spec = json!({"programming": {"interface": "swd"}}); - assert_eq!(summary("/nonexistent/b.kicad_pcb", &b, &spec, &Ok(())), "programming: U1 STM32G431CBT6, interface swd on reachable pins"); + assert_eq!(summary("/nonexistent/b.kicad_pcb", &b, &spec, &Ok(())), "programming: U1 STM32G431CBT6, interface swd with declared access checked"); assert!(summary("/nonexistent/b.kicad_pcb", &b, &spec, &Err(vec!["x".into()])).ends_with("1 problem(s) (the gate below)")); } -} + #[test] + fn usb_serial_jtag_cable_checks_real_connector_pins_and_target() { + let txt = BOARD.replace("STM32G431CBT6", "ESP32-S3-MINI-1U-N8"); + let txt = txt.replacen("(net 0 \"\")", "(net 0 \"\") (net 6 \"DP\") (net 7 \"DM\") (net 8 \"VBUS\")", 1); + let at = txt.rfind(')').unwrap(); + let extra = r#"(footprint "Connector_USB:USB_C_Test" (layer "F.Cu") (at 10 10) + (property "Reference" "J1") + (pad "A6" smd rect (at 0 0) (size 1 1) (layers "F.Cu") (net 6 "DP")) + (pad "A7" smd rect (at 1 0) (size 1 1) (layers "F.Cu") (net 7 "DM")) + (pad "A1" smd rect (at 2 0) (size 1 1) (layers "F.Cu") (net 4 "GND")) + (pad "A4" smd rect (at 3 0) (size 1 1) (layers "F.Cu") (net 8 "VBUS")))"#; + let txt = format!("{}{}{}", &txt[..at], extra, &txt[at..]); + let b = Board::from_text("/nonexistent/b.kicad_pcb", txt).unwrap(); + let mut spec = json!({"programming": {"interface":"usb-serial-jtag", "access":"usb-cable", + "connector":"J1", "targetRef":"U1", "target":"ESP32-S3-MINI-1U-N8", "boot":"Hold BOOT, reset, release BOOT", + "connectorPins":{"D+":{"pads":["A6"],"net":"DP"},"D-":{"pads":["A7"],"net":"DM"}, + "GND":{"pads":["A1"],"net":"GND"},"VBUS":{"pads":["A4"],"net":"VBUS"}}}}); + assert!(check("/nonexistent/b.kicad_pcb", &b, &spec).is_ok()); + spec["programming"]["connectorPins"]["D+"]["net"] = json!("DM"); + assert!(check("/nonexistent/b.kicad_pcb", &b, &spec).is_err()); + spec["programming"]["connectorPins"]["D+"]["net"] = json!("DP"); + spec["programming"]["connector"] = json!("U1"); + assert!(check("/nonexistent/b.kicad_pcb", &b, &spec).is_err()); + spec["programming"]["connector"] = json!("J1"); + spec["programming"]["targetRef"] = json!("U2"); + assert!(check("/nonexistent/b.kicad_pcb", &b, &spec).is_err()); + } + +}