← Commit history

#135: whole library in the seed, checked ADDs, asserts reported

Colby Knox ·f28f81af52 ·11d ago ·parent 28af550
3 files changed +71−7
CHANGELOG.md+14
@@ -3,6 +3,20 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.13.45 (2026-10-31)++Issue #135 (Andy Engevold, AI Flow schematic_run: every ADD failed with "No part available", then Fusion's+PlacePartWorker assetAdded assert, clicked away, and the run went on past the caller's timeout):+- `fusion_board_seed` embeds the WHOLE project library in Seed.sch (every deviceset, symbol, package and 3D package),+  not only the fixed parts' devices. Every later ADD is then from a library the design holds; on Fusion 2706 USE of a+  .lbr on disk is accepted but ADD finds nothing, which is what failed. `wholeLibrary:false` keeps the old seed.+- `fusion_schematic_run` ADDs from the design-held library (USE of the .lbr on disk only when the design does not hold+  it), checks after each ADD that the part is on the sheet, and stops at the first that did not land: `add_failed`+  with `failedAt`, `part` and why. The reply names `libraryFrom` (design or use).+- Every Fusion assertion the bridge clicks OK on is recorded; a verb during which a non-harmless assertion was raised+  (anything but the routing pusher.cpp one) fails with `fusion_assert` and `fusionAsserts`, instead of reporting+  success.+ ## 1.13.44 (2026-10-30)  - The import's needs_approval reply carries ab's own answer (`approvalAnswer`) and no longer claims a five-minute wait
rust/bridge/src/etiquette.rs+22
@@ -15,6 +15,14 @@ use std::time::{Duration, Instant}; use fusion_core::ab::{AbClient, Caller, Loopback};  static BADGED: AtomicBool = AtomicBool::new(false);+/// Every Fusion assertion the bridge clicked OK on (the background sentinel included), with when:+/// a verb's reply names the ones raised while it ran (issue #135: a fatal PlacePartWorker assert was+/// clicked away silently and the run went on).+static ASSERTS_SEEN: std::sync::Mutex<Vec<(f64, String)>> = std::sync::Mutex::new(Vec::new());+fn now_secs() -> f64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0) }+/// The routing assertion Fusion raises when a LINE completes a net's last airwire (pusher.cpp+/// inLocator) is informational; any other one means the command did not do its job.+pub fn assert_is_harmless(body: &str) -> bool { body.to_lowercase().contains("pusher.cpp") } static BADGED_HWND: std::sync::Mutex<i64> = std::sync::Mutex::new(0); /// `set_driving_badge`: the Adom mark on Fusion's taskbar button while Adom drives it. pub fn set_badge(caller: &Caller, hwnd: i64) -> bool {@@ -150,6 +158,7 @@ pub fn dismiss_unexpected_condition(ab: &dyn AbClient, caller: &Caller, pids: &[         let body = ab.call("desktop_find_controls", json!({"hwnd": h}), caller, 10).ok().map(|r| { let dd = if r["data"].is_object() { r["data"].clone() } else { r }; dd["controls"].as_array().or(dd["matches"].as_array()).cloned().unwrap_or_default().iter().filter_map(|c| c["name"].as_str()).filter(|n| n.to_lowercase().contains("unexpected condition")).map(str::to_string).next().unwrap_or_default() }).unwrap_or_default();         let clicked = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12).map(|r| r["invoked"].as_bool().unwrap_or(false) || r["success"].as_bool().unwrap_or(false)).unwrap_or(false);         eprintln!("[fusion-bridge] Fusion Electronics assertion dialog {}: {}", if clicked { "dismissed (OK)" } else { "seen, OK did not land" }, body.chars().take(160).collect::<String>());+        if let Ok(mut v) = ASSERTS_SEEN.lock() { v.push((now_secs(), body.clone())); if v.len() > 50 { v.remove(0); } }         return Some(if body.is_empty() { "Fusion Electronics has detected an unexpected condition".into() } else { body });     }     None@@ -289,6 +298,19 @@ pub fn post(state_dir: &Path, caller: &Caller, cmd: &str, args: &Value, mut res:             // the assertion lands a moment AFTER the command returns: look for it a few times             let mut found = None;             for i in 0..4 { found = dismiss_unexpected_condition(&ab, caller, pids); if found.is_some() { break; } if i < 3 { std::thread::sleep(Duration::from_millis(300)); } }+            let _ = found.take();+            // every assertion clicked while this verb ran (the sentinel's too): a harmless routing one is+            // noted; any other one fails the verb (#135: PlacePartWorker assetAdded = the ADD did not land)+            let seen: Vec<String> = ASSERTS_SEEN.lock().map(|v| v.iter().filter(|(t, _)| *t >= since).map(|(_, b)| b.clone()).collect()).unwrap_or_default();+            let fatal: Vec<&String> = seen.iter().filter(|b| !assert_is_harmless(b)).collect();+            if !fatal.is_empty() && res.is_object() {+                res["success"] = json!(false);+                if res["errorCode"].is_null() || res["errorCode"] == json!("") { res["errorCode"] = json!("fusion_assert"); }+                res["fusionAsserts"] = json!(seen);+                let e = res["error"].as_str().unwrap_or("").to_string();+                res["error"] = json!(format!("Fusion raised an internal error during this verb ({}); the bridge clicked OK so Fusion is not blocked, but the command did not complete. {e}", fatal[0].chars().take(160).collect::<String>()).trim().to_string());+            }+            let found = seen.into_iter().last();             if let Some(body) = found { if res.is_object() { res["fusionUnexpectedCondition"] = json!(body); let h = res["_hint"].as_str().unwrap_or("").to_string(); res["_hint"] = json!(format!("{h} Fusion Electronics raised its 'unexpected condition' assertion during this verb and the bridge clicked OK; checkpoint the board now (fusion_export_eagle_source) and verify the copper with fusion_routing_state.").trim().to_string()); } }         }     }
rust/bridge/src/routing_script.py+35−7
@@ -664,9 +664,20 @@ def sch_run(app, args):             if m.group(2) != libname: fail('library_mismatch', 'ADD names library %s but libraryPath is %s' % (m.group(2), libname))             if m.group(1).upper() not in devices: fail('unknown_device', 'No deviceset %s in %s' % (m.group(1), lbr))     done = []-    # (Fusion cannot take a library out of use from a command, so two in-use libraries with the same-    # name make ADD ask which one; keep project library names unique in a session)-    if lbr: app.executeTextCommand("Electron.run USE '%s';" % Path(lbr).as_posix())+    # ADD resolves from a library the design HOLDS (fusion_board_seed embeds the whole project library).+    # USE of the .lbr on disk is only the fallback when the design does not hold it: on Fusion 2706 that+    # USE is accepted but ADD then finds nothing (issue #135). (Fusion cannot take a library out of use+    # from a command, so two in-use libraries with the same name make ADD ask which one.)+    prod = app.activeProduct+    def part_names():+        try: return {prod.parts.item(i).name for i in range(prod.parts.count)}+        except Exception: return None+    held = set()+    try:+        for i in range(prod.libraries.count): held.add(prod.libraries.item(i).name)+    except Exception: pass+    used = 'design' if lbr and libname in held else ('use' if lbr else None)+    if used == 'use': app.executeTextCommand("Electron.run USE '%s';" % Path(lbr).as_posix())     pause = min(max(float(args.get('pauseMs', 0)), 0), 3000) / 1000.0     for c in cmds:         c = c.strip()@@ -675,9 +686,21 @@ def sch_run(app, args):         if c.startswith('VALUE '): c = 'SET CONFIRM YES; ' + c + ' SET CONFIRM OFF;'         # ADD of a device set the design already holds in another version asks "Ok to update?":         # the library on disk was just proven, so updating to it is the intent-        if c.startswith('ADD '): c = 'SET CONFIRM YES; ' + c + ' SET CONFIRM OFF;'+        is_add = c.startswith('ADD ')+        if is_add: c = 'SET CONFIRM YES; ' + c + ' SET CONFIRM OFF;'         app.executeTextCommand('Electron.run ' + c)         done.append(c)+        if is_add:+            # the part must be on the sheet now: stop at the first ADD that did not land, naming it,+            # instead of running on (#135 retried for minutes past the caller's timeout)+            want = SCH_FORMS[0][1].match(c.replace('SET CONFIRM YES; ', '').replace(' SET CONFIRM OFF;', '').strip()).group(3)+            names = part_names()+            if names is not None and want not in names:+                return {'success': False, 'mutated': len(done) > 1, 'errorCode': 'add_failed', 'failedAt': len(done) - 1, 'failedCommand': c, 'part': want,+                        'libraryFrom': used, 'librariesHeld': sorted(held), 'commands': len(done), 'documentName': name,+                        'error': 'ADD of %s did not land: Fusion found no such device in %s (%s). The commands before it ran.' % (want, libname,+                                 'the design holds that library, so the device is missing from it' if used == 'design' else 'the design does not hold that library and USE of the .lbr on disk does not make it addable on this Fusion'),+                        '_hint': 'Seed the design with fusion_board_seed (it embeds the whole library), or add the part to the design\'s library; then run the rest of the commands.'}         if pause:             app.executeTextCommand('Electron.ImmediateRepaint /full')             import adsk; adsk.doEvents(); time.sleep(pause)@@ -689,7 +712,7 @@ def sch_run(app, args):     ok = not missing     return {'success': ok, 'mutated': True, 'verified': ok, 'errorCode': None if ok else 'readback_mismatch',             'error': None if ok else 'parts not on the sheet after ADD: %s' % missing, 'documentName': name,-            'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done)}+            'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done), 'libraryFrom': used}   def board_bom(board, args):@@ -824,8 +847,13 @@ def board_seed(args):     lib = root.find('drawing/library'); LIB = Path(lbr).stem; lib.set('name', LIB)     fixed = args.get('fixed') or []     keep = {str(f.get('deviceset')).upper() for f in fixed}-    for d in list(lib.find('devicesets')):-        if d.get('name').upper() not in keep: lib.find('devicesets').remove(d)+    # the WHOLE library goes into the design by default: schematic_run ADDs every other part from the+    # library the design holds, because on Fusion 2706 USE of a .lbr on disk is accepted but ADD then+    # finds nothing (issue #135: every ADD failed, "No part available"). wholeLibrary:false keeps only+    # the fixed parts' devices, as before.+    if args.get('wholeLibrary') is False:+        for d in list(lib.find('devicesets')):+            if d.get('name').upper() not in keep: lib.find('devicesets').remove(d)     used_sym = {g.get('symbol') for d in lib.iter('deviceset') for g in d.iter('gate')}     used_pkg = {x.get('package') for d in lib.iter('deviceset') for x in d.iter('device')}     for x in list(lib.find('symbols')):